ProConcepts Layouts - Esri/arcgis-pro-sdk GitHub Wiki

//Open a layout… The layout functionality in ArcGIS Pro is delivered through the ArcGIS.Desktop.Layouts assembly. This assembly provides classes and members that support managing layouts, layout elements and working with layout views. This includes creating new layouts and layout elements, modifying existing elements, managing selections, and layout view control and navigation.

Language:      C#
Subject:       Layouts
Contributor:   ArcGIS Pro SDK Team <[email protected]>
Organization:  Esri, http://www.esri.com
Date:          10/06/2024
ArcGIS Pro:    3.4
Visual Studio: 2022

In this topic

Layout class

The Layout class represents a page layout in a project and provides access to basic layout properties, including page information, access to elements, and export methods. When working with layouts, one of the primary steps is to either reference an existing layout or create a new layout. A project can contain zero to many layouts. Within the application, existing layouts appear in the Catalog pane as individual project items, or as open views. Layout views are associated with a single layout. You can have multiple views for a single layout. For a project, only one view can be active at a time. The active view may, or may not be a layout view.

Create a New Layout

When you create a new layout using the SDK with the LayoutFactory.Instance.CreateLayout method, it creates a layout project item that automatically appears in the Contents pane. There are two overloads:

public class LayoutFactory : ILayoutFactory {

   //Create a layout specifying width and height in the specified "units".
   public Layout CreateLayout(double width, double height, LinearUnit units, 
           bool showRulers = true, double smallestRulerDivision = 0.5) {
        ...
   //Create a layout using a CIMPage to specify initial page parameters
   public Layout CreateLayout(CIMPage cimPage = null) {
      ...

Either specify the page parameters directly (overload 1) or use a CIMPage (overload 2). The CIMPage provides a greater level of granularity than using individual parameters, but is a bit more involved to use. Examples follow:

 QueuedTask.Run(()=> {
    //This is overload1, specifying a 17x11" page, show rulers is true and the minimum
    //dimension shown on the ruler is 1 inch
    var layout = LayoutFactory.Instance.CreateLayout(17, 11, LinearUnit.Inches, true, 1.0);
    layout.SetName("New Layout Name"); 

    //This is overload2 using a CIMPage
    var newPage = new CIMPage();
    newPage.Width = 17;
    newPage.Height = 11;
    newPage.Units = LinearUnit.Inches;
    newPage.ShowRulers = true;
    newPage.SmallestRulerDivision = 1.0;

    //To this point, the CIMPage simply duplicates the same settings we used on
    //overload 1 above...however, with a CIMPage, we can do more - such as defining
    //guides and margins
    var guides = new List<CIMGuide>() {
       new CIMGuide() { Orientation = Orientation.Vertical, Position = newPage.Width / 2},
       new CIMGuide() { Orientation = Orientation.Horizontal, Position = newPage.Height / 2}
    };
    newPage.Guides = guides.ToArray();

    var margin = new CIMMargin();
    margin.Left = margin.Right = margin.Top = margin.Bottom = 1.0;

    var layout2 = LayoutFactory.Instance.CreateLayout(newPage);
    layout2.SetName("New Layout Name2"); 

At the conclusion of the LayoutFactory.Instance.CreateLayout(), a layout has been created and added to the Layouts folder collection in the Catalog window. However, a layout pane has not been opened. Also, to save the new layout permanently to the Project, call Project.Current.SaveAsync().

Open a Layout

To open a layout, create or activate a layout view to host the layout using FrameworkApplication.Panes.CreateLayoutPaneAsync(layout). topic 19624. Pass in the layout to be shown on the view as the input parameter. The layout view pane will open and display its associated layout. The TOC will be populated with the layout element content. Note: CreateLayoutPaneAsync() must be called on the UI thread (not the QueuedTask) or an invalid operation exception will be thrown:

 //Open a layout
  var layout_sel = await QueuedTask.Run(() => {
    //Create the layout
    var layout = LayoutFactory.Instance.CreateLayout(17, 11, LinearUnit.Inches, true, 1.0);
    //TODO…add element content…
    ...
    return layout;
 });

 //Remember to call CreateLayoutPaneAsync on the UI Thread!
 await FrameworkApplication.Panes.CreateLayoutPaneAsync(layout_sel);

Retrieving an Existing Layout

To retrieve a layout stored in a project (and open it) use Project.Current.GetItems<T>(), topic 9198, where "T" is of type LayoutProjectItem. Use LINQ to filter the returned collection to just the layout item you are looking for. Next, call layoutProjectItem.GetLayout() to retrieve the layout and load it. The layout, once retrieved, can be passed to FrameworkApplication.Panes.CreateLayoutPaneAsync(layout) as before to open the layout.

 //Retrieve a layout from the project
 var layout_sel = await QueuedTask.Run(() => {
     //Call GetItems<T>, use LINQ to further refine the selection...
     var layoutItem = Project.Current.GetItems<LayoutProjectItem>().FirstOrDefault(
                                      			item => item.Name ==Layout1);
     //retrieve the layout from the LayoutProjectItem
     return layoutItem.GetLayout();
 }
 //open the layout...must be on the UI thread
 FrameworkApplication.Panes.CreateLayoutPaneAsync(layout_sel);

Once a layout is open and a layout view is the active view, addins can use the application context via the LayoutView.Active static property to get a handle to the current active layout view (or null if there is no active layout view) and LayoutView.Active.Layout to access the layout:

 //Reference a layout associated with an active layout view
 var layout = LayoutView.Active.Layout;
 if (lyt != null) {
   //TODO...manipulate layout
 }

After having a reference to a layout, the next logical step is to either add or modify layout content. We look at creation of layout content first.

Layout Element Creation

Layout content is represented as Elements, topic 11058. To create layout content, the ElementFactory is used. It provides a series of different macros for creating element content. Element content can be organized into the following categories:

  • Simple graphic elements, which includes point, line, and polygon, and text elements. Text elements are organized into point, line, or paragraph text.
  • Simple text graphic elements, which includes point, line (or curve), and paragraph text.
  • Map frames
  • Map surrounds, associated with a map frame, which includes legend, scale bar, north arrow, table frame, and chart frame.
  • Others, which include Attachment frames, picture graphics, and group elements (for organizing and "grouping" element content).

ElementInfo

Starting at 3.0, additional element properties can be specified using ElementInfo at the time of element creation. Previously, at 2.x, altering these properties required secondary calls to assign these properties after element creation. Element info allows you to specify upfront:

  • Element custom properties - a list of CIMStringMap key/value pairs
  • Anchor placement via ArcGIS.Core.CIM.Anchor values (TopLeftCorner, BottomMidPoint, LeftMidPoint, etc)
  • Default element rotation in degrees
  • Corner rounding specified as a percentage from 0.0 - 50.0% (applied to rectangular polygons otherwise it is ignored)

Example of using ElementInfo can be found in the following sections.

Graphic Elements

All simple graphic elements consist of a geometry and a symbol. In the case of text elements, they include text content as well. The primary macros for creating graphic element content are:

For any of the macros, you must provide the container to which the newly created element can be added. Valid containers include: a Layout, a GroupElement, and a GraphicsLayer*.

*Map frames, map surrounds, and attachments cannot be added to GraphicsLayers.

Examples follow:

 var layout = LayoutView.Active.Layout;
 //Must be on QueuedTask
 QueuedTask.Run(()=> {
  
 //1. Most basic - provide the container and a geometry. A default symbol and name are applied.
 var ge = GraphicFactory.Instance.CreateGraphicElement(layout, point_location);

 //Add a geometry and a symbol and a name
 var center = new Coordinate2D(2, 4);
 var circle_seg = EllipticArcBuilderEx.CreateCircle(
        new Coordinate2D(2, 4), 0.5, ArcOrientation.ArcClockwise, null);

 //2. make a circle graphic - provide the container, a geometry, and symbol. A default name is applied.
 var circle_poly = PolygonBuilderEx.CreatePolygon(PolylineBuilderEx.CreatePolyline(circle_seg));

 //Define a custom symbol
 var outline = SymbolFactory.Instance.ConstructStroke(ColorFactory.Instance.BlackRGB, 2.0, 
                                                                       SimpleLineStyle.Dash);
 var circle_sym = SymbolFactory.Instance.ConstructPolygonSymbol(
                              ColorFactory.Instance.RedRGB, SimpleFillStyle.Solid, outline);

 var ge_circle = GraphicFactory.Instance.CreateGraphicElement(layout, circle_poly, circle_sym);

 //3. Make a line graphic - provide the container, a geometry, symbol, and name.
 var plCoords = new List<Coordinate2D>();
 plCoords.Add(new Coordinate2D(1, 8.5));
 plCoords.Add(new Coordinate2D(1.66, 9));
 plCoords.Add(new Coordinate2D(2.33, 8.1));
 plCoords.Add(new Coordinate2D(3, 8.5));
 var poly_line = PolylineBuilderEx.CreatePolyline(plCoords);

 //Reference a line symbol in a style
 var style_item = Project.Current.GetItems<StyleProjectItem>().First(x => x.Name == "ArcGIS 2D");
 var symStyle = style_item.SearchSymbols(StyleItemType.LineSymbol, "Line with 2 Markers")[0];
 var lineSym = symStyle.Symbol as CIMLineSymbol;
 lineSym.SetSize(20);

 var ge_line = ElementFactory.Instance.CreateGraphicElement(
                                         layout, poly_line, lineSym, "New Line");

 //4. Make a text graphic with a placement point. A default symbol, text string, and name will be applied
 var coord2D = new Coordinate2D(3.5, 10);
 var ge_text = ElementFactory.Instance.CreateTextGraphicElement(
        layout, TextType.PointText, coord2D.ToMapPoint());

 //5. Make a text graphic with a placement point, symbol, text string, and name.
 var coord2D = new Coordinate2D(3.5, 10);
 var text_sym = SymbolFactory.Instance.ConstructTextSymbol(
                     ColorFactory.Instance.RedRGB, 32, "Arial", "Regular");

 var ge_text = ElementFactory.Instance.CreateTextGraphicElement(
        layout, TextType.PointText, coord2D.ToMapPoint(), text_sym, "This is the text", "New Point Text1");

 //6. Make a rectangle paragraph text graphic with a placement envelope, symbol, text string, and name.
 var ll = new Coordinate2D(3.5, 4.75);
 var ur = new Coordinate2D(5.5, 5.75);
 var env = EnvelopeBuilderEx.CreateEnvelope(ll, ur);

 //Create a text symbol
 var text_sym = SymbolFactory.Instance.ConstructTextSymbol(
                          ColorFactory.Instance.WhiteRGB, 10, "Arial", "Regular");
 //Create the graphic
 var ge_text2 = ElementFactory.Instance.CreateTextGraphicElement(
         layout, TextType.RectangleParagraph, env, text_sym, "This is the text", "New Rectangle Text1");

 //7. Make a polygon paragraph text graphic with a polygon, symbol, text string, and name.
 var plyCoords = new List<Coordinate2D>();
 plyCoords.Add(new Coordinate2D(3.5, 7));
 plyCoords.Add(new Coordinate2D(4.5, 7));
 plyCoords.Add(new Coordinate2D(4.5, 6.7));
 plyCoords.Add(new Coordinate2D(5.5, 6.7));
 plyCoords.Add(new Coordinate2D(5.5, 6.1));
 plyCoords.Add(new Coordinate2D(3.5, 6.1));
 var poly = PolygonBuilderEx.CreatePolygon(plyCoords);

 var text_sym = SymbolFactory.Instance.ConstructTextSymbol(
                   ColorFactory.Instance.GreyRGB, 10, "Arial", "Regular");
 string text = "Some Text String <BOL>forced to wrap to other</BOL>lines.";

 //Create the graphic
 var ge_text3 = ElementFactory.Instance.CreateTextGraphicElement(
        layout, TextType.PolygonParagraph, poly, text_sym, text, "New Polygon Text1");

 //8. Make a splined text graphic with a cubic bezier, symbol, text string, and name.
 var pt1 = new Coordinate2D(3.5, 7.5);
 var pt2 = new Coordinate2D(4.16, 8);
 var pt3 = new Coordinate2D(4.83, 7.1);
 var pt4 = new Coordinate2D(5.5, 7.5);
 var bezier = new CubicBezierBuilderEx(pt1, pt2, pt3, pt4);

 var bezier_line = PolylineBuilderEx.CreatePolyline(
                     bezier.ToSegment(), AttributeFlags.AllAttributes);

 var text_sym = SymbolFactory.Instance.ConstructTextSymbol(
                ColorFactory.Instance.BlackRGB, 24, "Comic Sans MS", "Regular");

 //Create the graphic 
 var ge_text4 = ElementFactory.Instance.CreateTextGraphicElement(
        layout, TextType.SplinedText, bezier_line, text_sym, "This is splined text", "Splined Text1");

In these examples, graphic elements are created using ElementInfo to pre-set various element properties upfront as part of creation. ElementInfo can be used with any element creation, not just graphic elements.

 var layout = LayoutView.Active.Layout;
 //Must be on QueuedTask
 QueuedTask.Run(()=> {

 //1. Create a circle graphic. Set the anchor to be the center point using an ElementInfo
 var center = new Coordinate2D(2, 4);
 var circle_seg = EllipticArcBuilderEx.CreateCircle(
        center, 0.5, ArcOrientation.ArcClockwise, null);
 var circle_poly = PolygonBuilderEx.CreatePolygon(PolylineBuilderEx.CreatePolyline(circle_seg));

 //Use an element info to specify the anchor
 var elemInfo = new ElementInfo() { Anchor = Anchor.CenterPoint };
 
 //Create the graphic element 
 var ge_circle = ElementFactory.Instance.CreateGraphicElement(
                   layout, circle_poly, null, "New Circle", true, elemInfo);

 //2. Create a point graphic. Specify a top right anchor, a 45.0 degree rotation and add
 // two custom properties
 var coord2D = new Coordinate2D(2.0, 10.0);

 //Reference a point symbol in a style
 var style_item = Project.Current.GetItems<StyleProjectItem>().First(x => x.Name == "ArcGIS 2D");
 var symStyle = style_item.SearchSymbols(StyleItemType.PointSymbol, "City Hall")[0];
 var pointSym = symStyle.Symbol as CIMPointSymbol;
 pointSym.SetSize(50);

 //Create the element info
 var elemInfo = new ElementInfo() {
      CustomProperties = new List<CIMStringMap>() {
           new CIMStringMap() { Key = "Key1", Value = "Value1"},
           new CIMStringMap() { Key = "Key2", Value = "Value2"}
      },
      Anchor = Anchor.TopRightCorner,
      Rotation = 45.0
  };
  //create the graphic element
  var ge_pt = ElementFactory.Instance.CreateGraphicElement(
        layout, coord2D.ToMapPoint(), null, String.Empty, true, elemInfo);

 //3. Create a rectangle with rounded corners, 45.0 rotation and a center point anchor
 var ll = new Coordinate2D(1.0, 4.75);
 var ur = new Coordinate2D(3.0, 5.75);
 var env = EnvelopeBuilderEx.CreateEnvelope(ll, ur);

 //Set symbolology, create and add element to layout
 var outline = SymbolFactory.Instance.ConstructStroke(
                   ColorFactory.Instance.BlackRGB, 5.0, SimpleLineStyle.Solid);
 var polySym = SymbolFactory.Instance.ConstructPolygonSymbol(
                   ColorFactory.Instance.GreenRGB, SimpleFillStyle.DiagonalCross, outline);

 var elemInfo = new ElementInfo() {
      Anchor = Anchor.CenterPoint,
      Rotation = 45.0,
      CornerRounding = 5.0
 };

 //create the graphic element
  var ge_pt = ElementFactory.Instance.CreateGraphicElement(
        layout, env, polySym, "New Rectangle", false, elemInfo);

Predefined Shapes and ArrowInfo

In these examples, graphic elements are created using predefined shapes using the predefined shape enum with ElementFactory.Instance.CreatePredefinedShapeGraphicElement to include circle, cloud, ellipse, half-circle, and triangle amongst others. To create a graphic element using one of the predefined shapes, a placement point or envelope (for the predefined shape) must be provided.

ElementFactory also provides for the creation of line cim graphics decorated with an arrow head on the start and end using ElementFactory.Instance.CreateArrowGraphicElement and the ArrowInfo class. To create a line graphic symbolized with arrow heads, a polyline must be specified along with the ArrowInfo to control the style, and placement, of the arrow head(s) on the input line.

Examples follow. Predefined shapes:

 var layout = LayoutView.Active.Layout;
 //Must be on QueuedTask
 QueuedTask.Run(()=> {

 //1. Create a graphic element using the Cloud predefined shape
 var ll = new Coordinate2D(4, 2.5);
 var ur = new Coordinate2D(6, 4.5);
 var env = EnvelopeBuilderEx.CreateEnvelope(ll, ur);

 var outline = SymbolFactory.Instance.ConstructStroke(ColorFactory.Instance.BlueRGB, 2);
 //Hollow fill
 var poly_sym = SymbolFactory.Instance.ConstructPolygonSymbol(null, outline);

 var elemInfo = new ElementInfo() {
        Anchor = Anchor.BottomLeftCorner
 };

 //Create the cloud graphic element
 var ge_cloud = ElementFactory.Instance.CreatePredefinedShapeGraphicElement(
        layout, PredefinedShape.Cloud, env, poly_sym, "Cloud1",  true, elemInfo);

 //2. Create a graphic element using the Rounded Rectangle predefined shape
 var ll = new Coordinate2D(6.5, 7);
 var ur = new Coordinate2D(9, 9);
 var env = EnvelopeBuilderEx.CreateEnvelope(ll, ur);
 
//Create the rounded rect graphic element
 var ge_rrect = ElementFactory.Instance.CreatePredefinedShapeGraphicElement(
        layout, PredefinedShape.RoundedRectangle, env, null, "Rounded Rect1", true);

 //3. Create a predefined shape using the Ellipse predefined shape
 Coordinate2D center = new Coordinate2D(2, 2.75);

 var outline = SymbolFactory.Instance.ConstructStroke(
                 ColorFactory.Instance.GreenRGB, 2.0, SimpleLineStyle.Dot);
 var ellipseSym = SymbolFactory.Instance.ConstructPolygonSymbol(
                      ColorFactory.Instance.GreyRGB, SimpleFillStyle.Vertical, outline);

 var ge_ellipse = ElementFactory.Instance.CreatePredefinedShapeGraphicElement(
        layout, PredefinedShape.Ellipse, center.ToMapPoint(), 0, 0, ellipseSym, "Ellipse1", false, 
            new ElementInfo() { Anchor = Anchor.TopRightCorner });

ArrowInfo examples:

 var layout = LayoutView.Active.Layout;
 //Must be on QueuedTask
 QueuedTask.Run(()=> {

 //1. Create a line graphic element with an arrow head on both ends. Use the default style 
 //at index 1 for the arrow heads
 var plCoords = new List<Coordinate2D>();
 plCoords.Add(new Coordinate2D(1, 8.5));
 plCoords.Add(new Coordinate2D(1.66, 9));
 plCoords.Add(new Coordinate2D(2.33, 8.1));
 plCoords.Add(new Coordinate2D(3, 8.5));
 var arrow_line = PolylineBuilderEx.CreatePolyline(plCoords);

 var arrowInfo = new ArrowInfo() {
      ArrowHeadKey = ArrowInfo.DefaultArrowHeadKeys[1],
      ArrowOnBothEnds = true,
      ArrowSizePoints = 30,
      LineWidthPoints = 15
 };

 var ge_line_arrows = ElementFactory.Instance.CreateArrowGraphicElement(
        layout, arrow_line, arrowInfo, "Arrow Line1", false);

 //2. Create a line graphic element with an arrow head on both ends. Use the
 // default style at index 8 for the arrow heads
 var plCoords = new List<Coordinate2D>();
 plCoords.Add(new Coordinate2D(1, 8.5));
 plCoords.Add(new Coordinate2D(1.66, 9));
 plCoords.Add(new Coordinate2D(2.33, 8.1));
 plCoords.Add(new Coordinate2D(3, 8.5));
 var arrow_line = PolylineBuilderEx.CreatePolyline(plCoords);

 var arrowInfo = new ArrowInfo() {
      ArrowHeadKey = ArrowInfo.DefaultArrowHeadKeys[8],
      ArrowOnBothEnds = false,//arrow head on the end only (not beginning)
      ArrowSizePoints = 24,
      LineWidthPoints = 12
 };

 var ge_line_arrow = ElementFactory.Instance.CreateArrowGraphicElement(
        layout, arrow_line, arrowInfo, "Arrow Line2", true);

GraphicFactory

Graphic elements can also be created via ElementFactory overloads that take a CIMGraphic - either a point, line, polygon, or text CIMGraphic. A CIMGraphic is, in its simplest form, allows a geometry and symbol to be pre-combined rather than having to specify them separately (CIMGraphics can also be added directly to the MapView graphics overlay via mapView.AddOverlay(graphic)).

Prior to 3.0, CIMGraphics must be constructed "by hand", whereas, at 3.0, a new factory is introduced: GraphicFactory to simplify CIMGraphic creation. CIMGraphics can be created using custom geometries, provided by the relevant addin code, or can be created using the same predefined shapes as ElementFactory. Legend patch graphics can also be created using the GraphicFactory.Instance.CreateLegendPatchGraphic method along with the ArcGIS.Core.CIM.PatchShape enum. To create a graphic using one of the predefined PatchShape enums, the bounding box (envelope) of the legend patch graphic must be provided (along with the relevant enum value).

The GraphicFactory provides overloads for creating:

  • Point, line, and polygon CIM graphics: GraphicFactory.Instance.CreateSimpleGraphic(geometry, symbol = null), topic 76447.
  • Text CIM graphics: GraphicFactory.Instance.CreateSimpleTextGraphic(textType, geometry, textSymbol = null, text = ""), topic 76448.
  • Predefined shapes CIM graphics: GraphicFactory.Instance.CreatePredefinedShapeGraphic(shapeType, location, width, height = 0.0, polySymbol = null), topic 76455 and GraphicFactory.Instance.CreatePredefinedShapeGraphic(shapeType, bounds, polySymbol = null), topic 76446.
  • Line CIM graphics with arrow heads applied: GraphicFactory.Instance.CreateArrowGraphic(arrowLine, arrowInfo), topic 76439.
  • Legend patches CIM graphics: GraphicFactory.Instance.CreateLegendPatchGraphic(patchShape, extent, patchSymbol = null), topic 76440.
  • Picture CIM graphics: GraphicFactory.Instance.CreatePictureGraphic(frameOrLocation, url), topic 76443
  • Extracting the CIM graphic outline from point, line, polygon, and text CIMGraphics, topic 14712

Additionally, GraphicFactory provides a utility method for retrieving the geometry from the underlying CIM graphic, which may prove useful: GraphicFactory.Instance.GetGeometry(graphic), topic 76449.

Examples follow:

 var layout = LayoutView.Active.Layout;
 //Must be on QueuedTask
 QueuedTask.Run(()=> {
 
//1. Create "simple" graphics: Point, Line, Polygon
 //Point
 var coord2D = new Coordinate2D(2.0, 10.0);
 //create the graphic
 var graphic = GraphicFactory.Instance.CreateSimpleGraphic(coord2D.ToMapPoint());
 //add to the layout
 var ge_point = ptElm = ElementFactory.Instance.CreateGraphicElement(layout, graphic);

 //Line
 var pt1 = new Coordinate2D(3.5, 7.5);
 var pt2 = new Coordinate2D(4.16, 8);
 var pt3 = new Coordinate2D(4.83, 7.1);
 var pt4 = new Coordinate2D(5.5, 7.5);
 var bez = new CubicBezierBuilderEx(pt1, pt2, pt3, pt4);
 var bezPl = PolylineBuilderEx.CreatePolyline(bez.ToSegment(), AttributeFlags.AllAttributes);

 //create the graphic
 var graphic = GraphicFactory.Instance.CreateSimpleGraphic(bezPl);
 //add to the layout
 var ge_curve = ElementFactory.Instance.CreateGraphicElement(layout, graphic);

 //Poly
 var ll = new Coordinate2D(1.0, 4.75);
 var ur = new Coordinate2D(3.0, 5.75);
 var env = EnvelopeBuilderEx.CreateEnvelope(ll, ur);

 //Set symbolology
 var outline = SymbolFactory.Instance.ConstructStroke(
                   ColorFactory.Instance.BlackRGB, 5.0, SimpleLineStyle.Solid);
 var polySym = SymbolFactory.Instance.ConstructPolygonSymbol(
                   ColorFactory.Instance.GreenRGB, SimpleFillStyle.DiagonalCross, outline);
 //create the graphic
 var ge_poly = GraphicFactory.Instance.CreateSimpleGraphic(env, polySym);
 //add to the layout
 var recElm = ElementFactory.Instance.CreateGraphicElement(layout, env, polySym, "New Rectangle");

 //Multiples...
 //poly
 var plyCoords = new List<Coordinate2D>();
 plyCoords.Add(new Coordinate2D(1, 7));
 plyCoords.Add(new Coordinate2D(2, 7));
 plyCoords.Add(new Coordinate2D(2, 6.7));
 plyCoords.Add(new Coordinate2D(3, 6.7));
 plyCoords.Add(new Coordinate2D(3, 6.1));
 plyCoords.Add(new Coordinate2D(1, 6.1));
 var poly = PolygonBuilderEx.CreatePolygon(plyCoords);

 //envelope
 var ll = new Coordinate2D(1.0, 4.75);
 var ur = new Coordinate2D(3.0, 5.75);
 var env = EnvelopeBuilderEx.CreateEnvelope(ll, ur);

 //point
 var coord2D = new Coordinate2D(2.0, 10.0);
 //create the graphics
 var g1 = GraphicFactory.Instance.CreateSimpleGraphic(poly);
 var g2 = GraphicFactory.Instance.CreateSimpleGraphic(env);
 var g3 = GraphicFactory.Instance.CreateSimpleGraphic(coord2D.ToMapPoint());

 //add to the layout
 var ge_elems = ElementFactory.Instance.CreateGraphicElements(
   layout, new List<CIMGraphic>() { g1, g2, g3 });
    
 //2. Simple graphics: text
 var graphic = GraphicFactory.Instance.CreateSimpleTextGraphic(
                   TextType.PointText, MapPointBuilderEx.CreateMapPoint(3, 5));
 //add to the layout
 var ge_text = ElementFactory.Instance.CreateGraphicElement(layout, graphic);

 //Paragraph
 var ll = new Coordinate2D(2.0, 1);
 var ur = new Coordinate2D(6.0, 2);
 var rect_env = EnvelopeBuilderEx.CreateEnvelope(ll, ur);

 //create the graphic
 var graphic = GraphicFactory.Instance.CreateSimpleTextGraphic(TextType.RectangleParagraph, rect_env, 
                                                        null, "Rect Graphic text");
 //add to the layout
 var ge_text_para = ElementFactory.Instance.CreateGraphicElement(layout, graphic, "SDK Rectangle Text");

 //Circle paragraph
  var center = new Coordinate2D(4.5, 4);
  var eabCir = new EllipticArcBuilderEx(center, 0.5, ArcOrientation.ArcClockwise);
  var circle = PolygonBuilderEx.CreatePolygon(
        PolylineBuilderEx.CreatePolyline(eabCir.ToSegment()));

 //create a symbol
 var text_sym = SymbolFactory.Instance.ConstructTextSymbol(
                      ColorFactory.Instance.GreenRGB, 10, "Arial", "Regular");
 string text = "Circle para text";
 //create the graphic
 var graphic = GraphicFactory.Instance.CreateSimpleTextGraphic(
                                 TextType.CircleParagraph, circle, text_sym, text);
 //add to the layout
 var ge_text_circle_para = ElementFactory.Instance.CreateGraphicElement(
                            layout, graphic, "New Circle Text", true);

 //Extract the geometry outline of various graphics...
 //Retrieve the outline for all of the graphic elements in the layout
 layout.SelectElements(geometry, SelectionCombinationMethod.New, false);
 var sel_elems = layout.GetSelectedElements().OfType<GraphicElement>().ToList();

 foreach(var ge in sel_elems) {
    var outline = GraphicFactory.Instance.GetGraphicOutline(layout, ge.GetGraphic());
    //add the outline back to the layout as a new graphic element
    var elem = ElementFactory.Instance.CreateGraphicElement(layout, outline);
 }

Predefined graphic shapes and arrows:

 var layout = LayoutView.Active.Layout;
 //Must be on QueuedTask
 QueuedTask.Run(()=> {

 //1. Predefined shapes
 var ll = new Coordinate2D(2.0, 1);
 var ur = new Coordinate2D(6.0, 2);
 var rect_env = EnvelopeBuilderEx.CreateEnvelope(ll, ur);

 //Construct a symbol
 var outline = SymbolFactory.Instance.ConstructStroke(
       ColorFactory.Instance.BlueRGB, 2);
 var poly_sym = SymbolFactory.Instance.ConstructPolygonSymbol(
        null, outline);//hollow fill

 //create the predefined shape
 var graphic = GraphicFactory.Instance.CreatePredefinedShapeGraphic(PredefinedShape.X, env, poly_sym);
 //create an element
 var ge_x = ElementFactory.Instance.CreateGraphicElement(layout, graphic);

 var coord2D = new Coordinate2D(3,3);
 //create the predefined shape, width of 20 points. Height will default to width in this case
 var graphic = GraphicFactory.Instance.CreatePredefinedShapeGraphic(PredefinedShape.Triangle,
                  coord2D.ToMapPoint(), 20.0);
 //create an element
 var ge_triangle = ElementFactory.Instance.CreateGraphicElement(layout, graphic);
 
 //2. Arrow heads
 var plCoords = new List<Coordinate2D>();
 plCoords.Add(new Coordinate2D(1, 8.5));
 plCoords.Add(new Coordinate2D(1.66, 9));
 plCoords.Add(new Coordinate2D(2.33, 8.1));
 plCoords.Add(new Coordinate2D(3, 8.5));
 var linePl = PolylineBuilderEx.CreatePolyline(plCoords);

 var arrowInfo = new ArrowInfo() {
   ArrowHeadKey = ArrowInfo.DefaultArrowHeadKeys[1],
   ArrowOnBothEnds = true,
   ArrowSizePoints = 30,
   LineWidthPoints = 15
 };
 //create the line graphic with arrow heads
 var graphic = GraphicFactory.Instance.CreateArrowGraphic(linePl, arrowInfo);
 //create an element
 var ge_line_elem = ElementFactory.Instance.CreateGraphicElement(
   layout, graphic, "Arrow Line");

Create a picture graphic:

 var layout = LayoutView.Active.Layout;
 //Must be on QueuedTask
 QueuedTask.Run(()=> {

 var pic_path = 
   @"https://www.esri.com/content/dam/esrisites/en-us/home/" +
    "homepage-tile-podcast-business-resilience-climate-change.jpg";

 //create the graphic
 var graphic = GraphicFactory.Instance.CreatePictureGraphic(new Coordinate2D(3.5, 2).ToMapPoint(), 
                                                              picPath);
 //create an element
 var ge_pic_elem = ElementFactory.Instance.CreateGraphicElement(layout, graphic);

Map Frame and Map Surrounds

To create map frames and associated surrounds, ElementFactory provides the following methods:

A map frame must be created first in order to associate a/the map surround with it. When creating a map frame, provide either its location (center point) or location along with the map instance with which it will be associated. Map surrounds include north arrows, legends, scale bars, table frames, and charts amongst others. When creating a surround, the relevant surround info class should be filled out with the name of its associated map frame, any associated layer or stand-alone table uris (from within the map of the associated map frame), styles, etc. The information required will vary with the type of surround being created. Custom properties, anchor placement, and rotation can be modified at creation time using an ElementInfo same as with graphic elements (as previously described). A list of the most common map surrounds and associated surround "infos" are:

Some examples follow:

 var map = ... 
 var layout = LayoutView.Active.Layout;
 //Must be on QueuedTask
 QueuedTask.Run(()=> {

  //1. create a map frame
  //aquire a map
  var mapItem = Project.Current.GetItems<MapProjectItem>().First();
  var map = mapItem.GetMap();

  var ll = new Coordinate2D(2.0, 4.5);
  var ur = new Coordinate2D(4.0, 6.5);
  var env = EnvelopeBuilderEx.CreateEnvelope(ll, ur);
  //create the map frame
  var map_frame = ElementFactory.Instance.CreateMapFrameElement(layout, env, map);

  //2. Create a legend
  //get associated map frame
  var map_frame = layout.FindElement("Map Frame1") as MapFrame;
  //Create the surround info - use default styling
  var legend_info = new LegendInfo() {
    MapFrameName = map_frame.Name
  };
  //Create the surround
  var legend_elem = (Legend)ElementFactory.Instance.CreateMapSurroundElement(
                         layout, new Coordinate2D(4, 9).ToMapPoint(), legend_info);

  //3. Create a North Arrow
  var center = new Coordinate2D(7, 5.5);

  //get associated map frame
  var map_frame = layout.FindElement("Map Frame1") as MapFrame;

  //Reference a North Arrow in a style
  var proj_style = Project.Current.GetItems<StyleProjectItem>().FirstOrDefault(item => item.Name == "ArcGIS 2D");
  var na_style = proj_style.SearchNorthArrows("ArcGIS North 10")[0];

  //Create the surround info
  var narrow_info = new NorthArrowInfo() {
    MapFrameName = map_frame.Name,
    NorthArrowStyleItem = na_style //Apply styling
  };

  //Create the surround
  var legend_elem = (Legend)ElementFactory.Instance.CreateMapSurroundElement(
                         layout, center.ToMapPoint(), narrow_info);

  //4. Create a scale bar
  //get associated map frame
  var map_frame = layout.FindElement("Map Frame1") as MapFrame;

  var ll = new Coordinate2D(5.0, 6);
  var ur = new Coordinate2D(6.0, 7);
  var env = EnvelopeBuilderEx.CreateEnvelope(ll, ur);

  //Reference a North Arrow in a style
  var proj_style = Project.Current.GetItems<StyleProjectItem>().FirstOrDefault(item => item.Name == "ArcGIS 2D");
  var sb_style = proj_style.SearchScaleBars("Alternating Scale Bar 1")[0];

  //Create the surround info
  var sbar_info = new ScaleBarInfo() {
        MapFrameName = mapFrame.Name
        ScaleBarStyleItem = sb_style
  };

  //Create the surround
  var sbar_elem = (ScaleBar)ElementFactory.Instance.CreateMapSurroundElement(
                         layout, env, sbar_info);

  //5. Create a table frame
  var ll = new Coordinate2D(1, 1);
  var ur = new Coordinate2D(5, 4);
  var env = EnvelopeBuilderEx.CreateEnvelope(ll, ur);

  var feat_layer = map.GetLayersAsFlattenedList().OfType<FeatureLayer>().First();

  var tableFrameInfo = new TableFrameInfo() {
        MapFrameName = mapFrameName,
        MapMemberUri = feat_layer.URI //Required
  };
  //Create the surround
  var table_elem = ElementFactory.Instance.CreateMapSurroundElement(layout, env.Center, tableFrameInfo,
        "New Table Frame", false, new ElementInfo() { Anchor = Anchor.CenterPoint });

Refer to [Activate a map frame)(#activate-a-map-frame) on how to activate/deactivate a map frame.

Grouping Elements

Elements can be created directly within an existing group element. You also have the ability to create new group elements as well via layout.CreateGroupElement(layout, elements, ...), topic 76404, either at the root level of the layout or within another group element. Passing a reference to the layout as the container will create the group at the root level of the layout, whereas passing in a group element will create the group element at the root level of the referenced group element.

  var layout = LayoutView.Active.Layout;
 //Must be on QueuedTask
 QueuedTask.Run(()=> {
   
   //Create an empty group element at the root level of the layout
   var grp1 = ElementFactory.Instance.CreateGroupElement(layout);

   //Create a group element containing "elems" at the root level of the layout
   var grp2 = ElementFactory.Instance.CreateGroupElement(layout, elems, "Group2");

   //Create an empty group element within the group element "grp1"
   var grp3 = LayoutElementFactory.Instance.CreateGroupElement(grp1);

Element Content

This section will look at:

  • Finding and Retrieving Elements
  • Selecting Elements
  • Modifying Elements
  • Styling Elements

Finding and Retrieving Elements

To find element content from a layout use either the Layout.FindElement(string name) or Layout.FindElements(IEnumerable<string> name). Element names are unique so only the element, or element(s) in the case of FindElement_s_, with the matching name will be returned. The entire element hierarchy of the layout is traversed to find the relevant element (or elements).

To retrieve elements, use either container.GetElements()* or container.GetElementsAsFlattenedList() where the container can be one of a layout, graphics layer, or group element*. GetElements() returns only the elements that are the immediate children of the container and element hierarchy is preserved. GetElementsAsFlattenedList(), on the other hand, recurses all nested groups to "flatten" the element hierarchy. It returns all elements from all nested groups as a single, flattened list. Hierarchy is not preserved. Note: just because an add-in uses GetElements or GetElementsAsFlattenedList over FindElement(s) doesnt mean that the retrieved list cannot be further refined. Addins can use LINQ on any of the returned element collections to further filter their content.

* GroupElement does not have a GetElements() method. It has an Elements property instead that provides exactly the same thing. The advantage of the Elements property is that it is "bindable" meaning, addins can bind to it for use in their own UIs. Layout also has the same property in-addition to GetElements().

 var layout = LayoutView.Active.Layout;
 //Must be on QueuedTask
 QueuedTask.Run(()=> {

   //Find specific elements by name
   var ge_rect = layout.FindElement("Rectangle") as GraphicElement;
   var na = layout.FindElement("North Arrow") as NorthArrow;
   var elems = layout.FindElements(new List<string> { "Legend", "Scale Bar", "Text2" });

  //Get elements retaining hierarchy
  var top_level_elems = 
      layout.GetElements()/layout.Elements/group_elem.Elements;//can bind XAML to Elements

  //Flatten hierarchy
  var all_elems = layout.GetElementsAsFlattenedList()/group_elem.GetElementsAsFlattenedList();

  //Use LINQ with any of the collections
  //Retrieve just those elements that are GraphicElements and are Visible
  var some_elems = all_elems.OfType<GraphicElement>().Where(ge => ge.IsVisible).ToList();

Selecting and Unselecting Elements

Element selection is accomplised by calling either SelectElement or SelectElement_s_ with the element or collection of elements to be selected. Elements will be selected regardless of their level within the given layout. SelectElement(s) can be called either on the layout or the layout view. The selected elements can be retrieved from either the layout or layout view via GetSelectedElements(). Unselection is exactly the opposite. Call UnSelectElement(s) on the layout or layout view with the element or collection of elements to be unselected. The entire element selection can be cleared by calling ClearElementSelection().

 var layoutView = LayoutView.Active;
 var layout = LayoutView.Active.Layout;
 //Must be on QueuedTask
 QueuedTask.Run(()=> {

   //Select/unselect some elements...
   var elems = layout.GetElementsAsFlattenedList();
   //select any element not a group element
   layout.SelectElements(elems.Where(e => !e.Name.StartsWith("Group")));//or layoutView
   layout.UnSelectElements(elems.Where(e => !e.Name.StartsWith("Group")));//or layoutView

   //Select/unselect all visible, graphic elements
   var ge_elems = all_elems.OfType<GraphicElement>().Where(ge => ge.IsVisible).ToList();
   layout.SelectElements(ge_elems);//or layoutView
   layout.UnSelectElements(ge_elems);//or layoutView

   //Select/unselect a specific element
   var na = layout.FindElement("North Arrow");
   layout.SelectElement(na);//or layoutView
   layout.UnSelectElement(na);//or layoutView

   //Select everything
   layoutView.SelectAllElements();//on LayoutView -only-

   //enumerate the selected elements
   foreach(var sel_elem in layout.GetSelectedElements()) {//or layoutView
     //TODO..process selection
   }

   //Clear the selection
   layout.ClearElementSelection();//or layoutView

Modifying Elements

Functionality for modifying elements is split between those methods accessed via the element itself versus those methods accessed on the layout. Element property values are, for the most part, accessed and modified via "Get" and "Set" method pairs on the element class itself. Some element properties are accessed via "Get" properties rather than via "Get" methods (eg Name). There are also a few element modification methods that are intrinsic to the layout (such as copy, delete, changing z order) rather than to an indivudal element and these methods are accessed off the layout instance instead.

The "basic" element properties are:

  • Name - element.Name, element.SetName("new name").
  • X coordinate - element.GetX(), element.SetX(9.5).
  • Y coordinate - element.GetY(), element.SetY(3.1).
  • Width - element.GetWidth(), element.SetWidth(4).
  • Height - element.GetHeight(), element.SetHeight(2).
  • Anchor - element.GetAnchor(), element.SetAnchor(Anchor.BottomLeftCorner | Anchor.CenterPoint | etc.).
  • LockedAspectRatio - element.GetLockedAspectRatio(), element.SetLockedAspectRatio(true | false).
  • Rotation - element.GetRotation(), element.SetRotation(45.0).
  • Anchor point - element.GetAnchorPoint(), element.SetAnchorPoint(anchor_pt_2D).
  • Visibility - element.IsVisible, element.SetVisible(true | false).
  • Z Order - element.ZOrder. Use layout methods to bring (elements) forward and send back.
  • MapSurrounds map frame - mapSurround.MapFrame, mapSurround.SetMapFrame(mapFrame) changes the associated map frame.

Simply call the "Get" method to retrieve the current (property) value* and the corresponding "Set" method to change it. Both "Get" and "Set" should be accessed on the MCT within a QueuedTask.Run.

*Generally speaking though certain property values use a "." property instead, eg Name, IsVisible, ZOrder, etc.

Element modification methods on the Layout (rather than on the element) include:

  • Group elements - layout.GroupElements(elements).
  • Un group elements - layout.UnGroupElement(group) and layout.UnGroupElements(groups).
  • Copy elements - layout.CopyElements(elements) and layout.CopyElements(groupElement, elements) copies to the "groupelement"*
  • Delete elements - layout.DeleteElement(element), layout.DeleteElements(elements), and layout.DeleteElements(Func<Element, bool> predicate)**.
  • Z Order - methods to bring (elements) forward, bring to front, send backward, and send to back.

*The group element must be a child of the target layout.

**The predicate is used to identify, or "filter", the elements to be deleted rather than explicitly providing a collection (of elements).

For grouping and ungrouping, all elements must belong to the layout within which they are being grouped (note: there is also ElementFactory.Instance.CreateGroupElement(container, elements, ...) as an alternative). Ditto for the ungrouping. The group or groups being ungrouped must also belong to the layout instance.

When copying elements, all elements being copied must share the same parent layout. They will all be copied to the root level of the target layout or group element (if that copy overload is being used) and their existing hierarchy will be maintained - meaning, if a group element is copied then so are all its children, and so on. If the source and target layout are the same (i.e. the elements to be copied come from the same layout on which "copy" is being invoked) then the elements will be duplicated and their names updated to ensure uniqueness.

Delete elements will delete the element or elements in the collection from the given layout. All elements in the collection must be contained within the layout on which "delete" is being invoked. If a group element is specified as an element to be deleted then all of its children will be deleted as well. Elements can be deleted from anywhere in the element hierarchy.

When modifying Z order of elements, the elements will be moved up ("Forward") or down ("Backward") in the z order of the layout element container. Elements higher in the Z order are drawn last whereas elements lower on the Z order are drawn first. If two elements overlap then the intersecting area of the element with the higher Z order (smaller absolute element.ZOrder value) will be drawn "on top of" the "lower" element. Elements are "Z ordered" within the TOC, with the elements on top of the Z order at the top of the TOC and vice versa for elements on the bottom. The same is true for an elements children (eg a group element). The Z order itself, accessible via element.ZOrder, is an integer ranging from (0 - n-1) where "n" equals the number of elements in the given container. O is at the top of the ZOrder (for a container) and n-1 is at the bottom.

It is also possible to check whether an element can be moved in the Z order by interrogating layout.CanBringForward(element) and layout.CanSendBackward(element). If an element cannot be moved further forward (alread at the top) or backward (already at the bottom) then CanBringForward/CanSendBackward would return false respectively.

Note: elements also contain a SetTOCPositionAbsolute(container, isTop = true | false) method that can be used to set the position of an element either at the top (isTop = true) or bottom (isTop = false) of the targetContainer. So for example, if the targetContainer is a Layout and the isTop parameter is set to true, the element will be placed at the top of the Contents pane. Likewise, if the targetContainer is a GroupElement and the isTop parameter is set to false, the element is placed at the bottom of the group element. Elements also contain a SetTOCPositionRelative(element, isAbove= true | false) allowing their Z order position to be changed relative (above - isAbove=true, or below - isAbove= false) the specified "relative" or target element.

Styling Elements

Layout elements and graphic elements can be styled using an existing StyleItem in a StyleProjectItem. A style item can be programmatically applied to existing layout elements (north arrow, scale bar, legend, table and chart frames) and to graphic elements (point, line, polygon and text). Graphic elements such as points, lines, polygons and texts exist both in Maps and in layouts.

StyleItems are represented by any one of the types defined by the StyleItemType enumeration. A StyleItem comes preconfigured with various settings such as color, orientation, angles, etc for that type (StyleItemType.NorthArrow, StyleItemType.Legend, etc). They offer a convenient way by which you can style various elements in your Map and Layouts. To get a specific StyleItem from a StyleProjectItem, use the following workflow:

  • Get the StyleProjectItem that has the style you need. ArcGIS Pro project has the following default StyleProjectItems: ArcGIS 2D, ArcGIS 3D, ArcGIS Colors, and ColorBrewer Schemes RGB
  • Use the SearchSymbols method on the StyleProjectItem to retrieve your StyleItem. Pass in the StyleItemType enumeration and search string for the item you are looking for.

The code snippet below shows how you can get the "ArcGIS North 18" style item from the "ArcGIS 2D" StyleProjectItem.

   //Use within QueuedTask.Run
   //Get the Style project items in the project
   var styleProjectItems = Project.Current?.GetItems<StyleProjectItem>();
   //Get the ArcGIS 2D Style Project Item
   var styleProjectItem = styleProjectItems.FirstOrDefault(s => s.Name == "ArcGIS 2D");
   //Get the north arrow style item you need
   var northArrowStyleItem = 
                 styleProjectItem.SearchSymbols(StyleItemType.NorthArrow, "ArcGIS North 18").FirstOrDefault();

Use the ApplyStyle method to change the existing style of an element. Pass in the the StyleItem you want to apply to the element as the input parameter. Call CanApplyStyle first to determine if the input style can be applied. In the snippet below, a North Arrow layout element is styled with a new "ArcGIS North 18" StyleItem using the ApplyStyle method.

    //Use within QueuedTask.Run   
    //Select a North arrow layout element
    var northArrowElement = layout.GetSelectedElements().OfType<NorthArrow>().FirstOrDefault();
    if (northArrowElement != null) {
      //Check if the input style can be applied to the element
      if (northArrowElement.CanApplyStyle(northArrowStyleItem))
         //Apply the style
         northArrowElement.ApplyStyle(northArrowStyleItem);
    }     

Layout elements that can be styled using the ApplyStyle method include the following:

  • Simple graphic elements, which includes point, line, and polygon, and text elements.
  • Map surrounds, associated with a map frame, which includes legend, scale bar, north arrow, table frame, and chart frame.

Other elements, which include Attachment frames, picture graphics, and group elements (for organizing and "grouping" element content) do not have an associated style item and hence cannot be styled with the ApplyStyle method. At Pro 3.2, grid and graticule styles cannot currently be applied to a map frame via mapFrame.ApplyStyle(....). Calling ApplyStyle on a map frame at 3.2 will result in an exception. Instead, use the ProSnippet for Applying a Style to Grids and Graticules if you need to programmatically apply a grid or graticule style to a map frame.

When applying a style to graphic elements - points, lines, polygons and texts - the default behavior is to apply the symbol size embedded in the style to the styled element (this also matches the behavior of the style gallery on the Pro UI). For point and text symbols, the default symbol size is usually 10 or 12 pts and for polygons and lines, the default symbol style line width is typically 1 pt. To retain the existing size of your graphic elements when they are being styled use the ApplyStyle method overload with a keepSymbolSize = true parameter value. For example:

   if (layoutPointElement.CanApplyStyle(pointStyleItem)) {           
       //Preserve current symbol size. 
       ge.ApplyStyle(pointStyleItem, true);           
   }

Map Frame Display Constraints

Display constraints for layout map frames can be modified to set limits on the frame's underlying map extent and its navigation behavior. If the layout contains more than one map frame, additional map frame constraints become available. These additional constraints enable one-directional links between two map frames that tie their views together. Each constraint allows you to select the map frame to which to link the current map frame. This selected map frame is called the linked map frame. If you update the extent of the linked map frame, the map frame where the constraint is set updates automatically.

layout-mapframe-constraints.png

Additional constraints are also available if a layout contains a spatial map series. A detailed description of the display constraints can be found in Map frame constraints in the Pro online help.

Programmatically setting a map frame extent involves configuring and setting a CIMAutoCamera, topic 440 via the map frame's SetAutoCamera method. The existing auto camera for a map frame can be retrieved via mapFrame.GetAutoCamera(). To determine whether a given auto camera's settings are valid and can be used to configure a given map frame, mapFrame.ValidateAutoCamera(autoCamera) should always be called.

A display constraint can not be applied to any empty map frame, a map frame that contains a scene*, and a map frame that is linked to a map series.

*Map frames containing scenes always have a display constraint type of "None".

 var layout = LayoutView.Active.Layout;
 var mf = layout.GetElementsAsFlattenedList().OfType<MapFrame>().First(
                                                mf => mf.Name == "MapFrame1");
 QueuedTask.Run(() => {
   
   //1. Display constraint "None"
   var autoCamera = mf.GetAutoCamera();
   autoCamera.Source = AutoCameraSource.None;

   //always validate and check we do not have a map series map frame
   if (mf.ValidateAutoCamera(autoCamera) && !mf.IsMapSeriesMapFrame())
      mf.SetAutoCamera(autoCamera);

   //2. Display constraint "FixedExtent"
   var autoCamera = mf.GetAutoCamera();
   autoCamera.Source = AutoCameraSource.Fixed;
   autoCamera.AutoCameraType = AutoCameraType.Extent;

   //set the extent for the frame - your extent will be different
   var extent = EnvelopeBuilderEx.CreateEnvelope(
     400748.62, 800296.4, 1310669.05, 1424520.74, mf.Map.SpatialReference);
   autoCamera.Extent = extent;
 
   //always validate and check we do not have a map series map frame
   if (mf.ValidateAutoCamera(autoCamera) && !mf.IsMapSeriesMapFrame())
      mf.SetAutoCamera(autoCamera);

   //3. Display constraint "FixedCenter"
   var autoCamera = mf.GetAutoCamera();
   autoCamera.Source = AutoCameraSource.Fixed;
   autoCamera.AutoCameraType = AutoCameraType.Center;

   var camera = mf.GetMapView(LayoutView.Active).Camera;
   var center = mf.GetViewCenter();
      
   //set a camera for the fixed center point
   //use values suitable for your particular map
   var camera2 = new CIMViewCamera() {
      Heading = 0,
      Pitch = -90,
      Roll = 0,
      Scale = 21169571,
      X = 855708,
      Y = 1112409,
      Z = double.NaN
   };
   autoCamera.Camera = camera2;
   //optionally, set a layer (in the map) to use for the center point
   var states = mf.Map.GetLayersAsFlattenedList().First(l => l.Name == "State_Polygons");
   autoCamera.IntersectLayerPath = states.URI;

   //always validate and check we do not have a map series map frame
   if (mf.ValidateAutoCamera(autoCamera) && !mf.IsMapSeriesMapFrame())
      mf.SetAutoCamera(autoCamera);

   //4. Display constraint "FixedCenterAndScale"
   var autoCamera = mf.GetAutoCamera();
   autoCamera.Source = AutoCameraSource.Fixed;
   autoCamera.AutoCameraType = AutoCameraType.CenterAndScale;

   var camera = mf.GetMapView(LayoutView.Active).Camera;
   var center = mf.GetViewCenter();

   //set a camera for the fixed center point and scale
   //use values suitable for your particular map
   var camera2 = new CIMViewCamera()
   {
      Heading = 0,
      Pitch = -90,
      Roll = 0,
      Scale = 21169571,
      X = 1310669.0 + ((400748.5 - 1310669.0) / 2.0),
      Y = 800296.4 + ((1424520.74 - 800296.4) / 2.0),
      Z = double.NaN
   };
   autoCamera.Camera = camera2;

  //always validate and check we do not have a map series map frame
  if (mf.ValidateAutoCamera(autoCamera) && !mf.IsMapSeriesMapFrame())
      mf.SetAutoCamera(autoCamera);

   //5. Display constraint "FixedScale"
   var autoCamera = mf.GetAutoCamera();
   autoCamera.Source = AutoCameraSource.Fixed;
   autoCamera.AutoCameraType = AutoCameraType.Scale;

   var camera = mf.GetMapView(LayoutView.Active).Camera;
   var center = mf.GetViewCenter();

   //set a camera for the fixed scale
   //use values suitable for your particular map
   var camera2 = new CIMViewCamera()
   {
      Heading = 0,
      Pitch = -90,
      Roll = 0,
      Scale = 21169571,
      X = 1310669.0 + ((400748.5 - 1310669.0) / 2.0),
      Y = 800296.4 + ((1424520.74 - 800296.4) / 2.0),
      Z = double.NaN
   };
   autoCamera.Camera = camera2;

   //always validate and check we do not have a map series map frame
   if (mf.ValidateAutoCamera(autoCamera) && !mf.IsMapSeriesMapFrame())
      mf.SetAutoCamera(autoCamera);

   //6. Display constraint "LinkedExtent" - there must be at least 2
   //map frames in the layout - the one on which to set the constraint 
   //and the one to link to
   var autoCamera = mf.GetAutoCamera();
   autoCamera.Source = AutoCameraSource.MapFrameLink;
   autoCamera.AutoCameraType = AutoCameraType.Extent;
   autoCamera.MapFrameLinkName = "MapFrame2";

   var states = mf.Map.GetLayersAsFlattenedList().First(l => l.Name == "State_Polygons");
   //optional, set intersecting layer to use for the linked extent
   autoCamera.IntersectLayerPath = states.URI;
   
   //always validate and check we do not have a map series map frame
   if (mf.ValidateAutoCamera(autoCamera) && !mf.IsMapSeriesMapFrame())
      mf.SetAutoCamera(autoCamera);

   //7. Display constraint "LinkedCenter" - there must be at least 2
   //map frames in the layout - the one on which to set the constraint 
   //and the one to link to
   var autoCamera = mf.GetAutoCamera();
   autoCamera.Source = AutoCameraSource.MapFrameLink;
   autoCamera.AutoCameraType = AutoCameraType.Center;
   autoCamera.MapFrameLinkName = "MapFrame2";

   var states = mf.Map.GetLayersAsFlattenedList().First(l => l.Name == "State_Polygons");
   //optional, set intersecting layer to use for the linked center
   autoCamera.IntersectLayerPath = states.URI;

   //always validate and check we do not have a map series map frame
   if (mf.ValidateAutoCamera(autoCamera) && !mf.IsMapSeriesMapFrame())
      mf.SetAutoCamera(autoCamera);

   //8. Display constraint "LinkedCenterAndScale" - there must be at least 2
   //map frames in the layout - the one on which to set the constraint 
   //and the one to link to
   var autoCamera = mf.GetAutoCamera();
   autoCamera.Source = AutoCameraSource.MapFrameLink;
   autoCamera.AutoCameraType = AutoCameraType.CenterAndScale;
   autoCamera.MapFrameLinkName = "MapFrame2";//controls center and scale

   //always validate and check we do not have a map series map frame
   if (mf.ValidateAutoCamera(autoCamera) && !mf.IsMapSeriesMapFrame())
      mf.SetAutoCamera(autoCamera);

   //9. Display constraint "LinkedScale" - there must be at least 2
   //map frames in the layout - the one on which to set the constraint 
   //and the one to link to
   var mf = GetMapFrame(mapFrame);
   var autoCamera = mf.GetAutoCamera();
   autoCamera.Source = AutoCameraSource.MapFrameLink;
   autoCamera.AutoCameraType = AutoCameraType.Scale;
   autoCamera.MapFrameLinkName = "MapFrame2";//scale

   //always validate and check we do not have a map series map frame
   if (mf.ValidateAutoCamera(autoCamera) && !mf.IsMapSeriesMapFrame())
      mf.SetAutoCamera(autoCamera);

   //10. Display constraint "LinkedMapSeriesShape" - there must be at least 2
   //map frames in the layout - the one which is set as the linked map series
   //frame and the one on which to set the constraint. There must also be
   //a map series defined on the layout.
   var mf = GetMapFrame(mapFrame);
   var autoCamera = mf.GetAutoCamera();
   autoCamera.Source = AutoCameraSource.MapSeriesLink;
   autoCamera.AutoCameraType = AutoCameraType.Extent;//set to the extent
                                                     //of the index feature

   //optional - can also set autoCamera.IntersectLayerPath

   //always validate and check we do not have a map series map frame
   if (mf.ValidateAutoCamera(autoCamera) && !mf.IsMapSeriesMapFrame())
      mf.SetAutoCamera(autoCamera);

   //11. Display constraint "LinkedMapSeriesCenter" - there must be at least 2
   //map frames in the layout - the one which is set as the linked map series
   //frame and the one on which to set the constraint. There must also be
   //a map series defined on the layout.

   var mf = GetMapFrame(mapFrame);
   var autoCamera = mf.GetAutoCamera();
   autoCamera.Source = AutoCameraSource.MapSeriesLink;
   autoCamera.AutoCameraType = AutoCameraType.Center;//set to center of
                                                     //index feature

   //optional - intersect "IntersectLayerPath" extent with index feature
   var states = mf.Map.GetLayersAsFlattenedList().First(l => l.Name == "State_Polygons");
   autoCamera.IntersectLayerPath = states.URI;

   //always validate and check we do not have a map series map frame
   if (mf.ValidateAutoCamera(autoCamera) && !mf.IsMapSeriesMapFrame())
      mf.SetAutoCamera(autoCamera);

Layout Metadata

Layout implements the ArcGIS.Desktop.Core.IMetadataInfo interface to provide read and write access to its metadata*. Layout metadata is saved with the layout in the .aprx. Metadata retrieved from the layout will be in xml format and will be styled according to whatever is the current metadata style in use. It is the add-in's responsibility to ensure that the rules associated with the current metadata style are met when editing metadata.

 //Supports access and update of metadata
 public interface IMetadataInfo {
   // Returns true if the metadata can be edited otherwise false
   bool GetCanEditMetadata();
   // Returns the xml formatted metadata
   string GetMetadata();
   //Set the metadata xml. The requirements of the current style
   //should be met
   void SetMetadata(string metadataXml);
 }

In this example, the layout metadata is retrieved and "set" without any changes if the layout metadata is editable. As a minimum, for layout metadata to be editable, the project access permissions must be read/write.

 var layout = LayoutView.Active.Layout; //Assume null check...

 QueuedTask.Run(() => {
   //read the layout metadata and set it back if the metadata
   //is editable
   var metadata_xml = layout.GetMetadata();
   if (layout.GetCanEditMetadata())
      layout.SetMetadata(metadata_xml);
 });

* Metadata access is also available off the ArcGIS.Desktop.Layouts.LayoutProjectItem object.

LayoutView class

A LayoutView is simply a view of a layout. Layout views are the primary interface used to display, navigate, select, and edit layout elements in the layout. The layout being visualized in the view can be accessed via the Layout property (e.g. layoutView.Layout). There are several scenarios that need to be evaluated when trying to reference a layout view:

  • A layout view can only exist if there is layout project item.
  • If a layout project item does exist, it doesn't mean that a layout view is open in the application.
  • Multiple layout views that reference the same layout can exist.
  • A layout view may be open but it may not be active.
  • The active view is not necessarily a layout view.

Activate an Existing View

In the Reference an existing layout section there is a sample that shows how to reference a layout associated with the active layout view. But another scenario involves making an already open layout view active. This requires that you iterate through the pane collection available to the FrameWorkApplication class, isolate the pane of interest, and then active it.

//Check to see if a layout view exists.  If it does, activate it.

//Confirm if layout exists as a project item
LayoutProjectItem layoutItem = Project.Current.GetItems<LayoutProjectItem>().FirstOrDefault(
                                                      item => item.Name.Equals("some layout"));
if (layoutItem != null)
{
  Layout lyt = await QueuedTask.Run(() => layoutItem.GetLayout());
        
  //Next check to see if a layout view is already open that referencs the Game Board layout
  foreach (var pane in ProApp.Panes)
  {
    var lytPane = pane as ILayoutPane; 

    //if not a layout view, continue to the next pane
    if (lytPane == null)  //if not a layout view, continue to the next pane
      continue;
    
    //if there is a match, activate the view
    if (lytPane.LayoutView.Layout == lyt) 
    {
      (lytPane as Pane).Activate();
      return;
    }
  }

Open a New View

As mentioned in the Open a Layout section, when a layout is created using the SDK, a layout view is not automatically generated. You must open the layout in a new view using the CreateLayoutPaneAsync method. Be sure to call this on the main GUI thread and use the await statement.

  //Must be on the UI thread
  await ProApp.Panes.CreateLayoutPaneAsync(layout);
  return;

Activate a Map Frame

A map frame on the current active layout view can be "activated" to allow users to interact with the map to perform tasks such as manual navigation, selection, and editing. Once a map frame is activated, the Layout UI experience changes. Tools for map navigation, selection, and editing become available and users work with the map within the context of the layout page. The rest of the layout becomes unavailable until the map frame is deactivated.

The following constraints apply for map frame activation:

  • A layout view must be the active view in Pro
  • Only one map frame can be activated at any one time
  • The map frame must contain a valid map
  • The map frame must be visible on the layout page associated with the current active layout view
  • Activation must be executed on the UI thread*

The following constraints apply for map frame deactivation:

  • A layout view must be the active view in Pro
  • Deactivation must be executed on the UI thread*

*This is the opposite of most API methods which normally require addins execute them on the MCT, Main CIM Thread, i.e. within a QueuedTask.Run

An example of activation and deactivation follows:

Activation:

  //a layout view must be active
  if (LayoutView.Active == null)
        return;
  //Check if "our" map frame is on the current layout and can be activated
  var layout = LayoutView.Active.Layout;
  var lv = LayoutView.Active;
  var mapFrame = layout.GetElementsAsFlattenedList().OfType<MapFrame>()
                       .First(mf => mf.Name == "Map 1");
  
  //Can we activate the map frame? - checks mapframe is visible, is on the active layout
  //view, has a valid map, and no other map frame (including this one) is currently activated
  //Note: we are on the UI thread!
  if (!lv.CanActivateMapFrame(mapFrame))
    return;
  //activate it
  lv.ActivateMapFrame(mapFrame);

  //Access the map and/or map view on the activated map frame
  //Note: Now we move to the QueuedTask as usual!
  QueuedTask.Run(() => {
    //zoom the map on the activated map frame
    var ext = lv.ActivatedMapView.Extent;
    var new_ext = GeometryEngine.Instance.Scale(ext, ext.Center, 2.0, 2.0);
    lv.ActivatedMapView.ZoomTo(new_ext, new TimeSpan(0, 0, 0, 0, 1500));
    //select some features
    lv.ActivatedMapView.SelectFeatures(new_ext, SelectionCombinationMethod.New);

    //note: activated map frame property is also available
    var activated_mf = LayoutView.Active.ActivatedMapFrame;//null if no activated map frame

Deactivation:

  //a layout view must be active
  if (LayoutView.Active == null)
        return;
  //call deactivation - no map frame param needed
  //Deactivation can only be called on the active layout view. Must be called on the
  //UI thread.
  LayoutView.Active.DeactivateMapFrame();//no-op if no mapframe is activated

  //...Or 
  //To check for an activated map frame first...
  if (LayoutView.Active.ActivatedMapFrame != null)
    LayoutView.Active.DeactivateMapFrame();
  

Element Selection and Navigation

The LayoutView class has several methods for managing layout element selection: ClearElementSelection, GetSelectedElements, SelectAllElements, and SelectElements. When either getting or setting the selection, a list collection is used. The LayoutView class also has multiple methods for navigating the layout.

// Find two graphic rectangle elements and set them to be selected and then zoom the page to the selection.    
if (LayoutView.Active != null)
{
  LayoutView lytView = LayoutView.Active;
  Layout lyt = await QueuedTask.Run(() => lytView.Layout);
  Element rec = lyt.FindElement("Rectangle");
  Element rec2 = lyt.FindElement("Rectangle 2");

  List<Element> elmList = new List<Element>();
  elmList.Add(rec);
  elmList.Add(rec2);
  
  //Set selection
  lytView.SelectElements(elmList);

  //Zoom to selection
  await QueuedTask.Run(() => lytView.ZoomToSelectedElements());
}

Layout and Element Events

Events associated with layouts and elements can be categorized into those associated with state changes (adds, deletes, updates, etc) and those associated with UI context changes (selection, unselection, activation, deactivation, etc).

For layout state changes, developers should use the ArcGIS.Desktop.Core.Events.ProjectItemsChangedEvent for layout adds and deletes and the ArcGIS.Desktop.Layouts.Events.LayoutEvent, topic 76585, for all other state changes. For UI context changes associated with layouts use LayoutViewEvent, topic 18643.

 //Detect adds and deletes of layouts
 ProjectItemsChangedEvent.Subscribe(OnProjectCollectionChanged)

 private void OnProjectCollectionChanged(ProjectItemsChangedEventArgs args) {
   if (args == null)
     return;
   var layoutItem = args.ProjectItem as LayoutProjectItem;
   if (layoutItem  == null) return;//not a layout

   Check what triggered the event and take appropriate action
   switch (args.Action) {
     //a layout is being added
     case System.Collections.Specialized.NotifyCollectionChangedAction.Add:
         //TODO handle layout being added
         //layoutItem is the layout item being added
         var layout_name = layoutProjectItem.Name; 
         var layout = layoutProjectItem.GetLayout();//must be on QTR to call GetLayout
         break;
     case System.Collections.Specialized.NotifyCollectionChangedAction.Remove:
         //TODO handle layout being removed
         //layoutItem is the layout item being removed
         var layout_name_del = layoutProjectItem.Name; //Dont call GetLayout()!
         break;
    }
 }

 //Detect changes to the layout map series, page, and modifications to
 //layout properties (eg name) - listen for the relevant hint
 ArcGIS.Desktop.Layouts.Events.LayoutEvent.Subscribe((args) => {
    var layout = args.Layout;//The layout that was changed

    //Check what triggered the event and take appropriate action
    switch (args.Hint) {
      case LayoutEventHint.MapSeriesPageChanged:
        //TODO handle map series changed
        break;
      case LayoutEventHint.MapSeriesRefreshed:
        //TODO handle page changed
        break;
      case LayoutEventHint.PageChanged:
        var old_page = args.OldPage;
        //TODO handle page changed
        break;
      case LayoutEventHint.PropertyChanged:
        //TODO handle property changed
        break;
    }
 });

 //For UI context changes associated with a layout, subscribe to the LayoutView
 //event - views activated/deactivated, views opened/closed
 ArcGIS.Desktop.Layouts.Events.LayoutViewEvent.Subscribe((args) =>{
  //get the affected view and layout
  var layout = args.LayoutView?.Layout;
  if (layout == null) {
     //FYI layoutview and/or layout can be null...
     //eg closed, deactivation
  }
  //Check what triggered the event and take appropriate action
  switch (args.Hint) {
    case LayoutViewEventHint.Closed:
      //Layout view closed
      break;
    case LayoutViewEventHint.Opened:
      //A LayoutView has been initialized
      break;
    case LayoutViewEventHint.Activated:
      break;
    case LayoutViewEventHint.Deactivated:
      break;
    case LayoutViewEventHint.Closing:
      //Set args.Cancel = true to prevent closing
      break;
    case LayoutViewEventHint.ExtentChanged:
      //Layout extent has changed
      break;
    case LayoutViewEventHint.DrawingComplete:
      break;
    case LayoutViewEventHint.PauseDrawingChanged:
      break;
  }
});
 

State and UI context for elements is consolidated into a single event: ArcGIS.Desktop.Layouts.Events.ElementEvent, topic 76571. Addins should subscribe to the event and interrogate the ElementEventArgs.Hint property to determine the event cause or "trigger":

ArcGIS.Desktop.Layouts.Events.ElementEvent.Subscribe((args) => {

  //Container can be Layout, GraphicsLayer, or GroupElement
  var layout = args.Container as Layout;
  if (layout == null)
    return;//we are only interested in layout
  var elems = args.Elements;//can be null or empty

  //Can also get elements/selected elements from the container
  //as needed
  var sel_elems = layout.GetSelectedElements();
  var all_elems = layout.GetElementsAsFlattenedList();

  //Check what triggered the event and take appropriate action
  switch (args.Hint) {
    case ElementEventHint.ElementAdded:
  	break;
    case ElementEventHint.ElementRemoved:
  	break;
    case ElementEventHint.SelectionChanged:
  	break;
    case ElementEventHint.MapFrameActivated:
  	break;
    case ElementEventHint.MapFrameDeactivated:
  	break;
    case ElementEventHint.MapFrameNavigated:
  	break;
    case ElementEventHint.PlacementChanged:
  	break;
    case ElementEventHint.PropertyChanged:
  	break;
    case ElementEventHint.StyleChanged:
  	break;
   }
});

LayoutTool

ArcGIS.Desktop.Layouts.LayoutTool is an abstract base class that provides a basic implementation of ArcGIS.Desktop.Framework.Contracts.Tool and represents a tool command used to perform interactive operations on a LayoutView or MapView.

The primary purpose of the layout tool is to add or edit graphics/elements in layouts and map views. Refer to the Graphic Elements section below for an in-depth description of this functionality.

The Layout tool also provides virtual methods that can be overridden to perform actions when keyboard and mouse events occur in the map view and layout view. It also provides properties that can be set to configure the behavior of the tool, as well as methods that wrap common functions when interacting with the view.

Declaring a Layout Tool

Layout tools are added to the <controls> section of the add-in Config.daml. Same as buttons, layout tools should be referenced within the groups that will contain them and the groups are, themselves, referenced within the individual tabs on which they should be shown. They have a corresponding code behind file which contains the layout tool "class" implementation and it should also be referenced in the tool daml. For example:

<modules>
    <insertModule id="..." ... caption="Module1">
      <tabs>
         <tab id="ProAppModule1_Tab1" caption="New Tab">
          <group refID="ProAppModule1_Group1"/><!-- reference the group here -->
        </tab>
      </tabs>
      <groups>
        <group id="ProAppModule1_Group1" caption="Group 1">
          <tool refID="ProAppModule1_PolygonGraphicsTool" size="large" />
        </group>
      </groups>
      <controls>
        <!-- define the tool element -->
        <tool id="ProAppModule1_PolygonGraphicsTool" caption="Polygon Graphics Tool" 
                                                     className="PolygonGraphicsTool" ... />
      </controls>
    </insertModule>
  </modules>

This is the layout tool code behind:

///<summary>PolygonGraphicsTool derives from the LayoutTool class. Note: PolygonGraphicsTool is 
///referenced in the className attribute of
///its <tool> element in the Config.daml</summary>
internal class PolygonGraphicsTool : LayoutTool 
{
        public PolygonGraphicsTool() 
        {
         ...
        }
    ...
}

The Pro SDK includes the ArcGIS Pro Layout Tool template which automates much of the daml and code behind class file definition required to create a basic layout tool. Run the Pro SDK layout tool template to auto-generate the tool daml and class file definition.

layout-tool.png

Using Conditions

Enabled/Disabled status of a layout tool can be controlled by adding a condition attribute on the tool daml element. The condition can be:

If the condition of a tool is set to be the daml id of a pane then only when instances of that type of pane are activated will your tool be enabled.

<controls>
        <!-- In this example, enable our tool whenever a layout view is active. Disable it otherwise -->
        <tool id="ProAppModule1_PolygonGraphicsTool" ... condition="esri_layouts_layoutPane">
          <tooltip .../>
        </tool>

Note: When the condition attribute is not set for a layout tool, it will be enabled when a map view or a layout view is active.

Creating a Sketch

Similar to the MapTool, the LayoutTool allows you to sketch a geometry in the map view or the layout view. Unlike the map tool, there is no IsSketch property in the layout tool so the layout tool is always sketching. The geometry type of the sketch can be set using the SketchType property. If you want the tool to snap while sketching set the UseSnapping property to true.

ElementFactory can consume either a Layout or a GraphicsLayer as its container (and a GroupElement). To assist the layout tool in identifying what is the current "active" container (i.e. a layout vs a graphics layer), the tool has an ActiveElementContainer for exactly that purpose. If a layout view is the active view, ActiveElementContainer will be set to the view's layout. However, if a map view is the active view, ActiveElementContainer will be set to the target graphics layer (if there is one). If there is no active container (eg the map view is a 3D scene) then ActiveElementContainer will be null

The purpose of ActiveElementContainer is to allow layout tools to implement their sketching logic in the OnSketchCompleteAsync handler independent of the current active view context: - mapview or layoutview.

This code snippet shows the basic outline of a layout tool, note that the OnSketchCompleteAsync handler works with either a mapview or a layout view:

internal class PolygonGraphicsTool : LayoutTool 
{        
   public PolygonGraphicsTool() {
      SketchType = SketchGeometryType.Polygon;
   }

   protected override Task OnToolActivateAsync(bool active)
   {
      return base.OnToolActivateAsync(active);
   }
	
    protected override Task<bool> OnSketchCompleteAsync(Geometry geometry)
    {
      //if we have an active container, place a polygon graphic
      if (ActiveElementContainer == null)
        Task.FromResult(true);

      return QueuedTask.Run(() => {
        //create the polygon graphic and add it to the active container
        //use default symbology
        var cimGraphic = GraphicFactory.Instance.CreateSimpleGraphic(geometry);
        ElementFactory.Instance.CreateGraphicElement(
          this.ActiveElementContainer, cimGraphic);
        return true;
      });
    }
 }

Here are a couple of different OnSketchCompleteAsync examples:

  SketchType = SketchGeometryType.Point;
  private string _text = ".....";

 //Place text
 protected override Task<bool> OnSketchCompleteAsync(Geometry geometry)
 {
   if (ActiveElementContainer == null)
     Task.FromResult(true);

   return QueuedTask.Run(() =>  {
     //create the text graphic and add it to the active container
     //use default symbology
     var cimGraphic = GraphicFactory.Instance.CreateSimpleTextGraphic(TextType.PointText,
                      geometry, null, _text);
     ElementFactory.Instance.CreateGraphicElement(this.ActiveElementContainer, cimGraphic);
     //The new text graphic element has been created
     //To switch to Pro's out of box "esri_layouts_inlineEditTool" 
     //to allow inline editing of the text, activate the tool below...
     //This tool will work on graphics on both map view and layouts
     //FrameworkApplication.SetCurrentToolAsync("esri_layouts_inlineEditTool");
     return true;
   });
 }

 //Curved line graphics
 SketchType = SketchGeometryType.BezierLine;

 protected override Task<bool> OnSketchCompleteAsync(Geometry geometry)
 {
    return QueuedTask.Run(() => {
        //create the curved line graphic and add it to the active container
        //use default symbology
        var cimGraphic = GraphicFactory.Instance.CreateSimpleGraphic(geometry);
        ElementFactory.Instance.CreateGraphicElement(
          this.ActiveElementContainer, cimGraphic);
        return true;
   });
 }

The GraphicElementSymbolPicker community sample demonstrates a complete workflow using the LayoutTool. This sample allows you to create graphics elements in a map view or a layout view using the same laytop tool.

Layout Tray Buttons

Just like a Map has tray buttons at the bottom of a map view, a layout has tray buttons at the bottom of a layout view. Whereas map views have numerous standard tray buttons, layout views only have a single standard tray button - Snapping. At 3.0, there is a Visual Studio template (ArcGIS Pro Layout Tray Button) to allow you to create your own custom layout tray buttons.

Layout Tray Buttons are the same as Map Tray Buttons except the class inherits from LayoutTrayButton and they are defined and registered in the esri_layouts_LayoutTrayButtons category in the config.daml file.

<updateCategory refID="esri_layouts_LayoutTrayButtons">
  <insertComponent id="TrayButtons_MyLayoutTrayButton" className="MyLayoutTrayButton">
    <content L_name="My Layout TrayButton" 
           largeImage="pack://application:,,,/ArcGIS.Desktop.Resources;component/Images/GenericButtonGreen32.png" 
           smallImage="pack://application:,,,/ArcGIS.Desktop.Resources;component/Images/GenericButtonGreen16.png" 
           L_tooltipHeading="My Layout TrayButton" L_tooltip="This is a sample tray button">
    </content>
  </insertComponent>
</updateCategory>

Below is a snapshot of the layout tray button area, containing the Snapping and a custom Layout Tray Button.

layoutTrayButtons

Controlling AutoClose Behavior

For tray buttons of type PopupToggleButton, the default behavior of the popup is to auto-close when the mouse travels outside the border of the window. If the popup hosts other controls such as a combobox, DateTimePicker control or a ColorPicker control that launch UI not completely confined by the popup window, there can be situations where the popup closes too early. Typically this would occur when your mouse moves to interact with the child control and that mouse location is outside the confines of the popup window.

The LayoutTrayButton class and the Visual Studio LayoutTrayButton template provide code to help manage this situation.

  • TrayButton.CanAutoClose - a flag on the Tray button to control its auto-close behavior.
  • A property defined in the popup ViewModel class - CanPopupAutoClose
  • Two event handlers in the popup View code behind - PopupOpened and PopupClosed. The PopupOpened event handler sets the CanPopupAutoClose property on the ViewModel class to false. And the PopupClosed event handler resets the CanPopupAutoClose to true.
  • Additional code in the tray button class - the PropertyChanged event handler for the ViewModel sets the TrayButton.CanAutoClose when the viewModel's CanpPopupAutoClose property changes.

TrayButton developers need only ensure that the provided PopupOpened and PopupClosed event handlers are hooked to the child controls Opened and Closed events. This means that the TrayButton.CanAutoClose is set to false when the controls Opened event occurs and is reset to true when the controls Closed event occurs.

Here is an illustration of a LayoutTrayButton whose popup contains a combobox.

The xaml for the view has the comboboxes DropDownOpened and DropDownClosed events hooked into the provided PopupOpened and PopupClosed handlers.

 <Border BorderThickness="1" BorderBrush="{DynamicResource Esri_Blue}">
    <StackPanel
            Margin="1"
            Width="Auto" Height="Auto"
            Background="Transparent">
      <!--this is the header-->
      <CheckBox Style="{DynamicResource Esri_CheckboxTrayButtonHeader}"        
                    Foreground="{DynamicResource Esri_Blue}"
                    Background="{DynamicResource Esri_Gray105}"
                    IsChecked="{Binding IsChecked, Mode=TwoWay}" >
        <TextBlock Style="{DynamicResource Esri_TextBlockTrayButtonHeader}"
              Text="{Binding Heading, Mode=OneWay}"/>
      </CheckBox>
      
      <!--content-->
      <Grid>
        <Grid.RowDefinitions>
          <RowDefinition Height="Auto"/>
          <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <TextBlock Grid.Row="0" Text="Colors"/>
        <ComboBox Margin="10,10" Grid.Row="1" Width="200" DropDownOpened="PopupOpened"  
                                                          DropDownClosed="PopupClosed" >
          <ComboBoxItem>red</ComboBoxItem>
          <ComboBoxItem>green</ComboBoxItem>
          <ComboBoxItem>b;ie</ComboBoxItem>
          <ComboBoxItem>black</ComboBoxItem>
          <ComboBoxItem>white</ComboBoxItem>
        </ComboBox>
      </Grid>
    </StackPanel>
  </Border>

Here is the definition of the PopupOpened and PopupClosed handlers in the Code behind file. This code is supplied by the Visual Studio template.

  //Event handlers to control auto close behavior of tray button 
  private void PopupOpened(object sender, EventArgs e)
  {
    var vm = (DataContext as testtraybuttonPopupViewModel);
    vm.CanPopupAutoClose = false;
  }

  private void PopupClosed(object sender, EventArgs e)
  {
    var vm = (DataContext as testtraybuttonPopupViewModel);
    vm.CanPopupAutoClose = true;
  }

The CanPopupAutoclose property is defined in the viewModel source file. This code is supplied by the Visual Studio template.

  //Property to control auto close behavior of tray button 
  private bool _canPopupAutoClose;
  public bool CanPopupAutoClose
  {
    get => _canPopupAutoClose;
    set => SetProperty(ref _canPopupAutoClose, value);
  }

And finally the PropertyChanged handler in the tray button source file that ensures the TrayButton.CanAutoClose property is set appropriately. This code is also supplied by the Visual Studio template.

    private void testtraybuttonPopupViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
      if (_popupVM == null)
        return;
      // make sure LayoutTrayButton class has correct checked state when it changes on the VM
      if (e.PropertyName == nameof(testtraybuttonPopupViewModel.IsChecked))
      {
        // Since we are changing IsChecked in OnButtonChecked
        //We don't want property notification to trigger (another) callback to OnButtonChecked
        this.SetCheckedNoCallback(_popupVM.IsChecked);
      }
      //Control auto close behavior of tray button 
      else if (e.PropertyName == nameof(testtraybuttonPopupViewModel.CanPopupAutoClose))
      {
        this.CanAutoClose = _popupVM.CanPopupAutoClose;
      }
    }

Now when the tray button popup is displayed and the combobox is opened, the popup will not auto-close when the mouse is moved outside the popup window. The auto-close behavior of the tray button popup is reset when the combobox is closed.

Review the ProGuide TrayButtons and sample TrayButtons to understand how to create your own custom tray buttons.

Map Series

Layout class and map series export options support multipage export.

The ArcGIS.Desktop.Layout.MapSeries class provides a static CreateSpatialMapSeries(...) factory method that generates a spatial map series for a given layout using the specified index layer (passed as a parameter). (Note: A map series is a collection of map pages (also known as map sheets) built from a single layout that represents a geographic area. A spatial map series uses an index layer where each feature in the layer represents the geographic area of an individual map sheet, one per feature.) Use the returned SpatialMapSeries class instance to refine the map series format options (extent options, margin settings, etc.) before "setting" it on the layout using the layout.SetMapSeries() method. Setting a map series onto a layout always overwrites any existing map series. The map series associated with a layout (if there is one) can be accessed off its layout.MapSeries property.

  //Construct map series on worker thread
  await QueuedTask.Run(() =>
  {
     //SpatialMapSeries constructor - required parameters
     var SMS = MapSeries.CreateSpatialMapSeries(layout, mapFrame, countiesLayer, "US Counties");
  
     //Set optional, non-default values
     SMS.CategoryField = "State";
     SMS.SortField = "Population";
     SMS.ExtentOptions = ExtentFitType.BestFit;
     SMS.MarginType = ArcGIS.Core.CIM.UnitType.PageUnits;
     SMS.MarginUnits = ArcGIS.Core.Geometry.LinearUnit.Centimeters;
     SMS.Margin = 1;

     layout.SetMapSeries(SMS);  //Will overwrite an existing map series.
  });

To export a map series, the MapSeriesExportOptions class can be used, in conjunction with an ExportFormat to create multi-page exports. For PDF and TIFF, the map series can be either be exported as individual files, one per map sheet, or as a single multi-page (or multi-image) file containing all of the (specified) map sheets. For all other export formats, individual pages are exported as individual files.

For example, the following code exports a custom range of pages from a map series to a multi-page PDF:

   //Export a map series with multiple pages to a single PDF from the active layout.
   var layout = LayoutView.Active.Layout;
   await QueuedTask.Run(() => {
      if (layout == null) return;

      // create the name of the pdf file 
      var pdf = System.IO.Path.Combine(
                     System.IO.Path.GetTempPath(), 
                     "US States.pdf");
      if (System.IO.File.Exists(pdf))
         System.IO.File.Delete(pdf);

      //Specify the exportFormat - PDF, 
      var exportFormat = new PDFFormat() {
         OutputFileName = pdf,
         Resolution = 300,
         DoCompressVectorGraphics = true,
         DoEmbedFonts = true,
         HasGeoRefInfo = true,
         ImageCompression = ImageCompression.Adaptive,
         ImageQuality = ImageQuality.Better,
         LayersAndAttributes = LayersAndAttributes.LayersAndAttributes
     };

     //Set up map series export options, 
     //this is new at 2.3
     var mapSeriesExportOptions = new MapSeriesExportOptions() {
          ExportPages = ExportPages.Custom, //All, Current, SelectedIndexFeatures
          CustomPages = "1-3",
          ExportFileOptions = ExportFileOptions.ExportAsSinglePDF,
     };
     //Note:
     //use ExportFileOptions.ExportMultipleNames or ExportFileOptions.ExportMultipleNumbers
     //for multi file exports. Your pdf file name is modified with a suffix per map sheet

     //Check to see if the path is valid and export
     if (exportFormat.ValidateOutputFilePath()) {
         layout.Export(exportFormat, mapSeriesExportOptions);
         //pop the pdf
         System.Diagnostics.Process.Start(pdf);
     }
 });

Layout Options

Layout options corresponding to the same options on the Pro options UI are available in the api via the static ApplicationOptions.LayoutOptions property - refer to ProConcepts Content - ApplicationOptions for more information. The LayoutOptions include:

 //Gets and sets the application layout options,
 public class LayoutOptions {
   //Gets and sets whether the insert tool stays active or reverts back to select after 
   //adding a new element.
   public bool KeepLastToolActive { get; set; }
   //Gets and sets whether a warning will be shown when deleting a map frame will
   //result in other elements in the layout being deleted.
   public bool WarnAboutAssociatedSurrounds { get; set; }
   //Gets and sets the default path for the location of the layout template gallery
   public string LayoutTemplatePath { get; set; }

   //Gets the application default layout guide color.
   public CIMColor DefaultGuideColor { get; }
   //Gets the application layout guide color. This method must be called on the MCT.
   //Use QueuedTask.Run.
   public CIMColor GetGuideColor();
   //Sets the layout guide color. This method must be called on the MCT. Use QueuedTask.Run.
   public void SetGuideColor(CIMColor color);
 }

Example usage:

 //Get current layout options
 var lastToolActive = ApplicationOptions.LayoutOptions.KeepLastToolActive;
 var warnOnSurrounds = ApplicationOptions.LayoutOptions.WarnAboutAssociatedSurrounds;
 //eg <Install_Path>\Resources\LayoutTemplates\en-US
 var gallery_path = ApplicationOptions.LayoutOptions.LayoutTemplatePath;
 var defaultGuideColor = ApplicationOptions.LayoutOptions.DefaultGuideColor;

 //Set layout options
 //keep graphic element insert tool active
 ApplicationOptions.LayoutOptions.KeepLastToolActive = true;
 //no warning when deleting a map frame results in other elements being deleted
 ApplicationOptions.LayoutOptions.WarnAboutAssociatedSurrounds = false;
 //path to .pagx files used as templates
 ApplicationOptions.LayoutOptions.LayoutTemplatePath = @"D:\data\layout_templates";
 QueuedTask.Run(() =>
 {
    var guideColor = ApplicationOptions.LayoutOptions.GetGuideColor();
    // set guide color
    ApplicationOptions.LayoutOptions.SetGuideColor(ColorFactory.Instance.RedRGB);
 });

Text and Graphics Elements Options

These option settings control the default text font and font style as well as the default point, line, polygon, and text symbols for use with graphic elements. They also apply to graphic elements being added to the map (in addition to the layout). They are available via the static ApplicationOptions.TextAndGraphicsElementsOptions property. The TextAndGraphicsElementsOptions include:

 //Gets and sets the application options for default font and graphic element symbology.
 public class TextAndGraphicsElementsOptions {
   //Gets the list of available fonts in the application for the Pro session. This
   //method must be called on the MCT. Use QueuedTask.Run.
   public IReadOnlyList<(string fontName, List<string> fontStyles)> GetAvailableFonts();
   //Gets the application default font family name and style. This method must be called on 
   //the MCT. Use QueuedTask.Run.
   public (string fontName, string styleName) GetDefaultFont();
   //Gets the application default line symbol. This method must be called on the MCT. Use QueuedTask.Run.
   public CIMLineSymbol GetDefaultLineSymbol();
   //Gets the application default point symbol. This method must be called on the MCT. Use QueuedTask.Run.
   public CIMPointSymbol GetDefaultPointSymbol();
   //Gets the application default polygon symbol. This method must be called on the MCT. Use QueuedTask.Run.
   public CIMPolygonSymbol GetDefaultPolygonSymbol();
   //Gets the application default text symbol. This method must be called on the MCT. Use QueuedTask.Run.
   public CIMTextSymbol GetDefaultTextSymbol();

   //Sets the application default font family name with a default style. This method must be called on the MCT. 
   //Use QueuedTask.Run.
   public void SetDefaultFont(string familyName);
   //Sets the application default font family name and style. This method must be called on the MCT. Use 
   //QueuedTask.Run.
   public void SetDefaultFont(string familyName, string styleName);
   //Sets the application default line symbol. This method must be called on the MCT. Use QueuedTask.Run.
   public void SetDefaultLineSymbol(CIMLineSymbol lineSymbol);
   //Sets the application default point symbol. This method must be called on the MCT. Use QueuedTask.Run.
   public void SetDefaultPointSymbol(CIMPointSymbol pointSymbol);
   //Sets the application default polygon symbol. This method must be called on the MCT. Use QueuedTask.Run.
   public void SetDefaultPolygonSymbol(CIMPolygonSymbol polygonSymbol);
   //Sets the application default text symbol. This method must be called on the MCT. Use QueuedTask.Run.
   public void SetDefaultTextSymbol(CIMTextSymbol textSymbol);
}

TextAndGraphicsElementsOptions is a little different from the other static options classes in that it also has a method (GetAvailableFonts() in this case) to provide a list of available (font) options as opposed to just "get/set" properties and/or "get/set" methods for the individual option values themselves. Note: calling TextAndGraphicsElementsOptions.GetAvailableFonts() is also equivalent to calling SymbolFactory.Instance.GetAvailableFonts() which returns the same list of values. The method is provided on TextAndGraphicsElementsOptions for convenience. Note too the use of explicit "Get" and "Set" methods rather than "Getter" and "Setter" properties as are commonly used on various of the other static options property classes. This is because getting/setting the TextAndGraphicsElementsOptions option values requires use of the Pro Main CIM Thread, MCT (accessed via QueuedTask.Run(...))

 //Get a list of all the available fonts (or use 'SymbolFactory.Instance.GetAvailableFonts()')
 QueuedTask.Run(() =>  {
    //A list of tuples of Font name + associated Font Styles, one tuple per
    //font, is returned
    var fonts = ApplicationOptions.TextAndGraphicsElementsOptions.GetAvailableFonts();
    StringBuilder sb = new StringBuilder();
    sb.AppendLine("Pro Fonts\r\n============================");
    foreach (var font in fonts) {
       var styles = string.Join(",", font.fontStyles);
       sb.AppendLine($"{font.fontName}, [{styles}]");
    }
    System.Diagnostics.Debug.WriteLine(sb.ToString());
 });

 //Get and Set the default TextAndGraphicsElementsOptions options values
 QueuedTask.Run(() => {
   //Get the default font (see also 'SymbolFactory.Instance.DefaultFont')
   var def_font = ApplicationOptions.TextAndGraphicsElementsOptions.GetDefaultFont();
   System.Diagnostics.Debug.WriteLine(
        $"\r\ndefault font: {def_font.fontName}, {def_font.styleName}");
      
   //Get the default graphics element symbols - point, line, poly, text
   var ptSymbol = ApplicationOptions.TextAndGraphicsElementsOptions.GetDefaultPointSymbol();
   var lineSymbol = ApplicationOptions.TextAndGraphicsElementsOptions.GetDefaultLineSymbol();
   var polySymbol = ApplicationOptions.TextAndGraphicsElementsOptions.GetDefaultPolygonSymbol();
   var textSymbol = ApplicationOptions.TextAndGraphicsElementsOptions.GetDefaultTextSymbol();

   //Set the options
   //Create some symbols
   var ptSymbol2 = SymbolFactory.Instance.ConstructPointSymbol(
        ColorFactory.Instance.RedRGB, 14, SimpleMarkerStyle.Diamond);
   var lineSymbol2 = SymbolFactory.Instance.ConstructLineSymbol(
        ColorFactory.Instance.RedRGB, 2, SimpleLineStyle.Dash);
   var polySymbol2 = SymbolFactory.Instance.ConstructPolygonSymbol(
        ColorFactory.Instance.RedRGB, SimpleFillStyle.DiagonalCross);
   var textSymbol2 = SymbolFactory.Instance.ConstructTextSymbol(
        ColorFactory.Instance.RedRGB, 12);

   //Set a default font. Use its default style
   ApplicationOptions.TextAndGraphicsElementsOptions.SetDefaultFont("tahoma");
   //or specify an explicit style
   ApplicationOptions.TextAndGraphicsElementsOptions.SetDefaultFont("tahoma", "bold");

   //Set default point, line, poly, text graphics element symbols
   ApplicationOptions.TextAndGraphicsElementsOptions.SetDefaultPointSymbol(ptSymbol2);
   ApplicationOptions.TextAndGraphicsElementsOptions.SetDefaultLineSymbol(lineSymbol2);
   ApplicationOptions.TextAndGraphicsElementsOptions.SetDefaultPolygonSymbol(polySymbol2);
   ApplicationOptions.TextAndGraphicsElementsOptions.SetDefaultTextSymbol(textSymbol2);
 });
⚠️ **GitHub.com Fallback** ⚠️