Select to view content in your preferred language

problem printing a Legend control

960
7
08-25-2011 11:09 AM
TerryGiles
Frequent Contributor
I have a button in my application which allows the user to print the map using the standard Silverlight PrintDocument class (acutally it 'clones' the map onto another user control which looks like our standard map tempalte).  I'm trying to add a 2nd page to the print doc which contains a legend control.  I'm able to get the 2nd page to print but the legend control aways has the busy indicator next to the layer names (see attached image).  Is there a way to track when the legend has completed building it's tree and contains the layer/sublayer names & symbology? 

The only other way I can think to do it is to use WebClient.DownloadStringAsynch() against each layer's legend (http://server/arcgis/rest/services/Service/Legend?f=pjson) and pull out the layer names and image bytes and put them into my own TreeView...  Anyone have any other ideas?

Thanks,

Terry
0 Kudos
7 Replies
JenniferNery
Esri Regular Contributor
I don't see a single event that gets fired when BusyIndicator is no longer visible but you should have access to the LegendItemsViewModel.PropertyChanged where this property changes: http://help.arcgis.com/en/webapi/silverlight/apiref/ESRI.ArcGIS.Client.Toolkit~ESRI.ArcGIS.Client.To...
0 Kudos
TerryGiles
Frequent Contributor
Thanks for the idea Jennifer.  I'm working on that but am don't doing something correctly I think.  My current code is below where I'm creating the legend and trying to hook into the PropertyChanged event for each LayerItemViewModel in the legend but my handler never fires.  Looking in to it some more, my Legend's LayerItems collection is null, but the LayerIDs are correct and I've been using that to drop out Tiled MapServices as they don't have a legend with symbology.  Am I missing something in how I'm creating the legend?

Thanks again,
Terry

       //_pp is a user control with the map on it I print
       private void btnOK_Click(Object sender, RoutedEventArgs e)
        {
            _LayerComplete = 0;
            _LayerCount = 0;

            //get map and legend prep'd
            _pp = _Parent.Printpage;
            _pp.setMap(_Map);

            _lgnd = new Legend();
            _lgnd.ShowOnlyVisibleLayers = true;
            _lgnd.LayerItemsMode = Legend.Mode.Tree;
            _lgnd.Map = _pp.mapPrint;
            

            (_pp.LayoutRoot.FindName("txtTitle") as TextBlock).Text = txtTitle.Text;
            (_pp.LayoutRoot.FindName("txtDate") as TextBlock).Text = "Created: " + DateTime.Now.ToShortDateString();

            //drop tiled services since they dont have a legend with symbology
            List<string> layIDs = new List<string>();
            foreach (var lyr in _pp.mapPrint.Layers)
            {
                if (!(lyr is ArcGISTiledMapServiceLayer))
                {
                    layIDs.Add(lyr.ID);
                }
            }
            layIDs.Reverse();
            _lgnd.LayerIDs = layIDs.ToArray();

            foreach (LayerItemViewModel livm in _lgnd.LayerItems)  
            {
                livm.PropertyChanged += livm_PropertyChanged; //never happens LayerItems is empty
            }
}
        void livm_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            MessageBox.Show(e.PropertyName + " changed");
            //real code should see which prop changed..if B
        }

0 Kudos
JenniferNery
Esri Regular Contributor
I think you can use Legend.Refreshed to get to the LayerItems: http://help.arcgis.com/en/webapi/silverlight/samples/start.htm#LegendWithTemplates. I can forward your question to Dominique so he can reply to you when he gets back.
0 Kudos
DominiqueBroux
Esri Frequent Contributor
IsBusy property of LayerItemViewModel indicates if the legend infos are being requested.

When IsBusy changes, the propertychanged event is fired, so at first glance your code should work.
However as LayerItems is empty, you don't subscribe to any event so that can't work.

So the question is why LayerItems is empty? From your first screenshot showing layer items with busy indicator, layeritems should not be empty. Are you sure that you array layIDs is not empty? (sidenote : layerIds.Reverse(); has no effect, you can use _lgnd.LayerIDs = layIDs.Reverse().ToArray(); instead).

As Jennifer mentionned, there is another option that can be simpler : use the legend Refreshed event.
Each time the legend of a layer is available, this event is fired. In the handler you might check the number of items with 'IsBusy == true'.
0 Kudos
TerryGiles
Frequent Contributor
Hi Jennifer & Dominique,

Thanks for the reviewing my code and the suggestions.  I still cannot explain why the legend's LayerItems is null as the string array layIds is populated correctly using the code snippet I posted below - I've stepped through it in debug mode & piped it out using   System.Diagnostics.Debug.WriteLine and it's contains the names of the layers I would expect.

I'm now trying to hook into the legend's refreshed event as you suggested but am having problems here, which I believe are related to how I'm trying to tie everything together, since I'm trying to print 2 pages.  The basic logic flow (code is below) is currently as follows -

  1. user clicks btnPrint;

  2. create a new legend in code, tie it to a map on a user control (_pp.mapPrint in the code) & add a handler to its Refreshed event;

  3. create a new PrintDocument and add a handler for it's PrintPage event; call PrintDocument.Print()

  4.   PrintPage handler fires - sets the user control with the map to the PrintPageEventArgs PageVisual property;

  5. Remove the current PrintPage handler & add PrintPage2 handler

  6. in PrintPage2 handler set the Legend's LayerIDs to just the non-tiled map services in the map

  7.   (here's where I stuck) .... try to wait for the Legend refreshed to fire and examine the IsBusy property of the LayerItemViewModel which fired the Refresh - increment a counter tracking how many are complete

  8.   when counter of complete == counter of # of map layers pass the Legend to the PrintPageEventArgs PageVisual property and reset HasMorePages to false so the doc will print


I seem to not be catching the Legend refreshed event properly - is this a threading issue or is my logic flawed, or both??

Thanks again for the help!
Terry



        private void btnOK_Click(Object sender, RoutedEventArgs e)
        {
            _LayerComplete = 0;
            _LayerCount = 0;


            //get map and legend prep'd
            _pp = _Parent.Printpage;
            _pp.setMap(_Map);
            _pp.Visibility = Visibility.Visible;
            (_pp.LayoutRoot.FindName("txtTitle") as TextBlock).Text = txtTitle.Text;
            (_pp.LayoutRoot.FindName("txtDate") as TextBlock).Text = "Created: " + DateTime.Now.ToShortDateString();

            _lgnd = new Legend();
            _lgnd.ShowOnlyVisibleLayers = true;
            _lgnd.LayerItemsMode = Legend.Mode.Tree;
            _lgnd.Map = _pp.mapPrint;
            _lgnd.Refreshed += _lgnd_Refreshed;

             _doc = new PrintDocument();

            _doc.PrintPage +=  doc_PrintPage1;
            _doc.EndPrint += doc_EndPrint;
            _doc.Print("AweMap_Export");

        }

        void _lgnd_Refreshed(object sender, Legend.RefreshedEventArgs e)
        {
            e.LayerItem.PropertyChanged += (s,evnt) =>
            {
                if (!(s as LayerItemViewModel).IsBusy)
                {
                    System.Diagnostics.Debug.WriteLine((s as LayerItemViewModel).Layer.ID + " is no longer busy");
                    _LayerComplete++;
                }
                else
                {
                    System.Diagnostics.Debug.WriteLine((s as LayerItemViewModel).Layer.ID + " is busy");
                }
            };

            //have also tried this with no luck - 

            //if (!e.LayerItem.IsBusy)
            //{
            //    System.Diagnostics.Debug.WriteLine(e.LayerItem.Layer.ID + " is no longer busy");
            //    _LayerComplete++;
            //}
            //else
            //{
            //    System.Diagnostics.Debug.WriteLine(e.LayerItem.Layer.ID + " is busy");
            //}
        }

        void doc_PrintPage1(object sender, PrintPageEventArgs args)
        {
            System.Diagnostics.Debug.WriteLine("start of page1 print)");

            _pp.Width = args.PrintableArea.Height;
            _pp.Height = args.PrintableArea.Width;
            _pp.UpdateLayout();

            if (rbLand.IsChecked.GetValueOrDefault(true))
            {
                //rotate to landscape
                TransformGroup transformGroup = new TransformGroup();
                transformGroup.Children.Add(new RotateTransform() { Angle = 90 });
                transformGroup.Children.Add(new TranslateTransform() { X = args.PrintableArea.Width });
                _pp.RenderTransform = transformGroup;
            }

            //swap handler for 2nd page w/ legend control
            _doc.PrintPage -= doc_PrintPage1;
            _doc.PrintPage += doc_PrintPage2;

            args.PageVisual = _pp;
            args.HasMorePages = true;
        }

        void doc_PrintPage2(object sender, PrintPageEventArgs args)
        {
            System.Diagnostics.Debug.WriteLine("start of page2 print)");
 
            //drop tiled services since they dont have a legend with symbology
            List<string> layIDs = new List<string>();
            foreach (Layer lyr in _pp.mapPrint.Layers.Where(t => !(t is ArcGISTiledMapServiceLayer)))
            {
                layIDs.Add(lyr.ID);
                _LayerCount++;
            }
            layIDs.Reverse();
            _lgnd.LayerIDs = layIDs.ToArray();

            //code below doesn't seem to change outcome, although if I don't set _lgnd.LayerIDs above  the LayerItems will be null in the loop below..      
      
            //foreach (Layer lyr in _pp.mapPrint.Layers.Where(t => !(t is ArcGISTiledMapServiceLayer)))
            //{
            //    _LayerCount++;
            //    LayerItemViewModel livm = new LayerItemViewModel(lyr);
            //    // livm.PropertyChanged += livm_PropertyChanged;
            //    _lgnd.LayerItems.Add(livm);
            //}


            int lcnt = 0;
            int lcomp = -1;

            while (lcomp < lcnt)
            {
                // this results in an endless loop as _LayerComplete is never updated 
                lcnt = _LayerCount;
                lcomp = _LayerComplete;
            }

            args.PageVisual = _lgnd;
            args.HasMorePages = false;

        }
0 Kudos
DominiqueBroux
Esri Frequent Contributor
I think I got your issue 🙂

The legend is doing asynchronous requests to get the legend infos and these requests are only done when the legend is put in the visual tree.
It means that the requests will be done after you set : args.PageVisual = _lgnd;
but this is too late because the print is done synchronously just after the PrintPage handler has been executed.

The workaround is to put the legend in the visual tree of your application (either in your mainpage or in a popup window) before returning it as PageVisual.

To make simple for a first test, you can put the legend in a popup window in the 'btnOK_Click' handler.
With a little chance, the legend will be ready when the second page prints.

If the legend is niot ready in docprint_page2, you will have to call:
           args.PageVisual = null; // nothing to print for now
           args.HasMorePages = true; // retry later
          Thread.Sleep(1000); // wait to give a chance to the legend to be ready
so docprint_page2 will be called again later.

Hope this help.
0 Kudos
TerryGiles
Frequent Contributor
Hello Dominique,

Adding the legend to a user control & thus the visual tree did the trick. 

A huge THANK YOU to both you and Jennifer. 

So far in testing the legend is always ready by the time I get to printing the 2nd page.  Modified code below in case it helps anyone else.  I made 3 other short changes - added a busy indicator, added a checkbox so the user could skip printing the legend (smaller export size), and set the Legend's background to white instead of the default gradient also for smaller export size & cleaner printing.


Thanks again,
Terry

        private void btnOK_Click(Object sender, RoutedEventArgs e)
        {
            busy.IsBusy = true;

            _LayerComplete = 0;
            _LayerCount = 0;

            //get map and legend prep'd
            _pp = _Parent.Printpage;
            _pp.setMap(_Map);
            _pp.Visibility = Visibility.Visible;
            (_pp.LayoutRoot.FindName("txtTitle") as TextBlock).Text = txtTitle.Text;
            (_pp.LayoutRoot.FindName("txtDate") as TextBlock).Text = "Created: " + DateTime.Now.ToShortDateString();

            _lgnd = new Legend();
            _ppl = _Parent.PrintLegend;
            _ppl.LayoutRoot.Children.Add(_lgnd);
            _lgnd.ShowOnlyVisibleLayers = true;
            _lgnd.LayerItemsMode = Legend.Mode.Tree;
            _lgnd.Map = _pp.mapPrint;
            _lgnd.Refreshed += _lgnd_Refreshed;
            _lgnd.Background = new SolidColorBrush(Colors.White);  //overwrite default gradient - smaller output size

            //drop tiled services since they dont have a legend with symbology
            List<string> layIDs = new List<string>();
            foreach (Layer lyr in _pp.mapPrint.Layers.Where(t => !(t is ArcGISTiledMapServiceLayer)))
            {
                layIDs.Add(lyr.ID);
                _LayerCount++;
            }
            layIDs.Reverse();
            _lgnd.LayerIDs = layIDs.ToArray();

             _doc = new PrintDocument();
            _doc.PrintPage +=  doc_PrintPage1;
            _doc.EndPrint += doc_EndPrint;
            _doc.Print("AweMap_" + txtTitle.Text);

        }

        void _lgnd_Refreshed(object sender, Legend.RefreshedEventArgs e)
        {
            //see if LIVM that fired refreshed is no longer busy; increment layer complete counter
            e.LayerItem.PropertyChanged += (s,evnt) =>
            {
                if ( (!(s as LayerItemViewModel).IsBusy) && evnt.PropertyName.ToLower() == "isbusy")
                {
                    _LayerComplete++;
                }
            };

        }

        void doc_PrintPage1(object sender, PrintPageEventArgs args)
        {
            _pp.Width = args.PrintableArea.Height;
            _pp.Height = args.PrintableArea.Width;
            _pp.UpdateLayout();

            if (rbLand.IsChecked.GetValueOrDefault(true))
            {
                //rotate to landscape
                TransformGroup transformGroup = new TransformGroup();
                transformGroup.Children.Add(new RotateTransform() { Angle = 90 });
                transformGroup.Children.Add(new TranslateTransform() { X = args.PrintableArea.Width });
                _pp.RenderTransform = transformGroup;
            }

            args.PageVisual = _pp;

            //see if user selected to include legend:D
            if (chkLegend.IsChecked.GetValueOrDefault(true))
            {
                //swap handler for 2nd page w/ legend control
                _doc.PrintPage -= doc_PrintPage1;
                _doc.PrintPage += doc_PrintPage2;
                args.HasMorePages = true;
            }
            else
            {
                args.HasMorePages = false;
            }
        }

        void doc_PrintPage2(object sender, PrintPageEventArgs args)
        {
            _pp.Width = args.PrintableArea.Height;
            _pp.Height = args.PrintableArea.Width;
            _lgnd.Width = _ppl.Width - 50;
            _lgnd.Height = _ppl.Height - 50;

            if (_LayerComplete < _LayerCount)
            {
                System.Diagnostics.Debug.WriteLine("Waiting for Legend...");
                System.Threading.Thread.Sleep(1000);
                args.PageVisual = null;
                args.HasMorePages = true;
            }
            else
            {
                args.PageVisual = _lgnd;
                args.HasMorePages = false;
            }

        }

0 Kudos