Exposing Feature Class from (file) GDB into WPF app as Feature Layer

4342
17
12-22-2011 08:08 AM
Labels (1)
BKuiper
Occasional Contributor III
Hello,

what would be the best way to expose a feature class which is stored in a (file) geodatabase in a WPF ESRI Map Control as a Feature Layer using ArcGIS Runtime.

Currently we are using a empty map file (.mxd), add a reference to the feature class and save it as a new map file (.mxd), convert the .mxd to a map package with runtime support (.mpk) and then consume it as a LocalArcGISFeatureLayer.

Would you agree that this is the best and preferred ESRI way ?
0 Kudos
17 Replies
MichaelBranscomb
Esri Frequent Contributor
Hi,

Yes, that is the recommended route. You can choose to reference the File Geodatabase from the Map Package, in which case the MPK will refer to the original File GDB location or alternatively include the File GDB within the Map Package. UNC paths and/or mapped drives are also supported which can make referencing the File GDB easier.

The new dynamic layers capability in Beta 2 also allows you to add new feature classes directly to a running LocalMapService. Unfortunately we didn't manage to include a sample for this in the new sample application but here is some code which should help you get started. This new dynamic layers functionality (also in ArcGIS for Server 10.1) operates on a per request basis - no changes are made to the underlying service and other users will not be affected.

## Default constructor ##

public MainWindow()
        {
            InitializeComponent();

            try
            {

            // get path to data relative to exe
            var dataFolder = System.IO.Path.GetFullPath(System.IO.Path.Combine(
              System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location),
              @"..\..\..\.."));

            var mapPackagePath = System.IO.Path.Combine(dataFolder, "Europe_Population_Density.mpk");
            if (!File.Exists(mapPackagePath))
            {
                MessageBox.Show("Map Package not found");
                return;
            }

            _LocalMapService = new LocalMapService(System.IO.Path.Combine(dataFolder, "Europe_Population_Density.mpk"));


// Make sure dynamic layers are enabled in this service
            _LocalMapService.EnableDynamicLayers = true;

            // Register workspaces to be used with this service.
            _LocalMapService.DynamicWorkspaces.Add(new WorkspaceInfo()
            {
                FactoryType = WorkspaceFactoryType.FileGDB,
                Id = "Europe_Population_DensityGDB", // a string to identify this workspace when repointing layers

                // Connection string must use "DATABASE=" to point to the fileGdb
                // API will automatically insert "DATABASE=" if it is not found in the connection string
                // API will resolve relative paths if they are used to refer to workspace
                // Workspaces are File GDBs, Folders (for Shapefiles) or Enterprise Geodatabases.
                ConnectionString = "DATABASE=" + System.IO.Path.Combine(dataFolder, "Europe_Population_Density.gdb"),
            });

            _LocalMapService.StartAsync((mapService) =>
            {
                var layer = new ArcGISLocalDynamicMapServiceLayer(_LocalMapService);
                layer.ImageFormat = ArcGISDynamicMapServiceLayer.RestImageFormat.PNG32;

                layer.InitializationFailed += (s, ex) =>
                {
                    MessageBox.Show("Failed to init layer: " + layer.InitializationFailure.ToString());
                };

                layer.Initialized += (s, ex) =>
                {
                    if (layer.InitializationFailure == null)
                        _layer = layer;
                    //UpdateMapButton_Click(null, null);
                };

                MapControl.Layers.Add(layer);
            });
            }
            catch (Exception ex)
            {
                var msg = ex.Message;
            }
        }

## Button Click event ##

private void UpdateMapButton_Click(object sender, RoutedEventArgs e)
        {
       
            selectedItem = ((System.Windows.Controls.ComboBoxItem)FeatureclassComboBox.SelectedItem);

            string fcName = selectedItem.Content.ToString().Split(new Char[] { ' ' })[0];

            // The following code will get the dynamic layer info collection from the layer (service)
            // and update the first item with a new DynamicLayerInfo object to change the data
            // source of an existing feature layer within the service. Alternatively you can create a
            // brand new DynamicLayerInfoCollection and begin adding new DynamicLayerInfos to this
            // collection. Doing this will effectively remove the existing layers and display only the new
            // feature layers defined.   
            var layers = _layer.CreateDynamicLayerInfosFromLayerInfos();
            layers[0] = new DynamicLayerInfo()
            {
                ID = 0,
                Source = new LayerDataSource()
                {
                    DataSource = new TableDataSource()
                    {
                        DataSourceName = fcName,
                        WorkspaceID = "Europe_Population_DensityGDB"
                    }
                }
            };
            _layer.DynamicLayerInfos = layers;

            LayerDrawingOptions layerDrawOpt = new LayerDrawingOptions();
            layerDrawOpt.LayerID = 0;

            layerDrawOpt.Renderer = GetClassBreaksRenderer("POP_DENSIT");
            _layer.LayerDrawingOptions = new LayerDrawingOptionsCollection() { layerDrawOpt };

            _layer.Refresh();
        }


## Get Class Breaks Renderer ##

The code above uses another couple of methods to create a Class Breaks Renderer which is used for displaying the data thematically:

private ClassBreaksRenderer GetClassBreaksRenderer(string attributeField)
        {
            ClassBreaksRenderer classBreaksRenderer = new ClassBreaksRenderer() { Attribute = attributeField };

            Symbol baseSymbol = GetSimpleFillSymbol(Colors.LightGray, Colors.LightGray, 1);

            GenerateDataClassesTask generateDataClassesTask = new GenerateDataClassesTask(_LocalMapService.UrlMapService + "/0");

            GenerateDataClassesParameters generateDataClassesParameters = new GenerateDataClassesParameters()
            {
                ClassificationDefinition = new ClassBreaksDefinition()
                {
                    BaseSymbol = baseSymbol,
                    ClassificationField = attributeField,
                    ColorRamp = new ColorRamp()
                    {
                        Algorithm = Algorithm.HSVAlgorithm,
                        From = _FromColor,
                        To = _ToColor   
                    },
                    BreakCount = 6,
                    ClassificationMethod = ClassificationMethod.NaturalBreaks,
                },
            };
            try
            {
                GenerateDataClassesResult generateDataClassesResult = generateDataClassesTask.Execute(generateDataClassesParameters);
                classBreaksRenderer = generateDataClassesResult.Renderer as ClassBreaksRenderer;
            }
            catch (Exception exception)
            {
                MessageBox.Show("Error: " + exception.Message);
            }

            return classBreaksRenderer;
        }

        private SimpleFillSymbol GetSimpleFillSymbol(Color fillColor, Color borderColor, double borderThickness)
        {
            SimpleFillSymbol fillSymbol = new SimpleFillSymbol()
            {
                BorderBrush = new SolidColorBrush(borderColor) {Opacity =0.75 },
                BorderThickness = borderThickness,
                Fill = new SolidColorBrush(fillColor)
            };
            return fillSymbol;
        }

In the code above _FromColor and _ToColor are just class members from the System.Windows.Media namespace and in some other code in this example are updateable via a combobox.

Color _FromColor = Colors.Yellow;
Color _ToColor = Colors.Red;


I hope that should get you started with dynamic layers.

Cheers

Mike
0 Kudos
BKuiper
Occasional Contributor III
Thanks Mike. That's really helpful.

Enjoy the Holidays!
0 Kudos
BKuiper
Occasional Contributor III
Hi Mike,

I'm an colleague from B Kuiper and working on the same project. I had a look at this.

Is it possible to get Europe_Population_Density.mpk?

Also there seems to be some code missing that populates the combo box.

Thanks
0 Kudos
MichaelBranscomb
Esri Frequent Contributor
Hi,

The ComboBoxes are actually just populated in XAML (although there are obviously other, more elegant ways to achieve this):

<Window x:Class="DynamicLayersDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:esri="http://schemas.esri.com/arcgis/client/2009"
        Title="ArcGIS Runtime SDK for WPF" Height="350" Width="525" WindowState="Maximized">
    <Grid>
    <esri:Map x:Name="MapControl" Background="LightGray">
    </esri:Map>
        <Grid HorizontalAlignment="Left" VerticalAlignment="Top" Margin="0,15,15,0" >
            <Rectangle Fill="#77919191" Stroke="Gray"  RadiusX="10" RadiusY="10" Margin="0,0,0,5" >
                <Rectangle.Effect>
                    <DropShadowEffect/>
                </Rectangle.Effect>
            </Rectangle>
            <Rectangle Fill="#FFFFFFFF" Stroke="DarkGray" RadiusX="5" RadiusY="5" Margin="10,10,10,15" />
            <StackPanel Orientation="Vertical" Margin="20,20,20,20" HorizontalAlignment="Left" >
                <TextBlock Text="Dynamic Layers Demo" HorizontalAlignment="Center" FontWeight="Bold" Margin="5,0,5,5" FontSize="18"/>
                <TextBlock x:Name="InformationTextBlock" Text="Choose a new feature class and color ramp from the list below:"
                           Width="200" TextAlignment="Left" TextWrapping="Wrap" Margin="5,0,5,5" FontSize="16"/>
                <ComboBox x:Name="NutsLevelsComboBox" SelectedIndex="0" Margin="5,0,5,5" FontSize="16">
                    <ComboBoxItem>NUTS0 (country level)</ComboBoxItem>
                    <ComboBoxItem>NUTS1 (3-7m people)</ComboBoxItem>
                    <ComboBoxItem>NUTS2 (0.8-3m people)</ComboBoxItem>
                    <ComboBoxItem>NUTS3 (0.15-0.8m people)</ComboBoxItem>
                </ComboBox>
                <ComboBox x:Name="colorComboBox" SelectedIndex="0" Margin="5,0,5,5" FontSize="16">
                    <ComboBoxItem>Yellow to Red</ComboBoxItem>
                    <ComboBoxItem>Green to Blue</ComboBoxItem>
                </ComboBox>
                <Button x:Name="UpdateMapButton" Click="UpdateMapButton_Click" Margin="5,0,5,5" FontSize="16">Update Map</Button>
            </StackPanel>
        </Grid>
    </Grid>
</Window>


Regarding the European Population Density Map Package - I simply created a new Map Package (with support for the ArcGIS Runtime) from the data you can download on ArcGIS.com: http://www.arcgis.com/home/item.html?id=cf3c8303e85748b5bc097cdbb5d39c31


Cheers

Mike
0 Kudos
BKuiper
Occasional Contributor III
Hi Mike,

thanks for taking the time to respond.

Let me start by clarifying what i'm trying to achieve.

Basically I just want to consume Feature classes from a File Geodatabase or SDE and display them on the MapControl. The Feature classes are generated during runtime, and I would like to consume them without having to use a MPK. Is this possible?

I got the example you provided working, but it doesn't quite work the way i would want to.

here is the download: http://bjorn.kuiper.nu/upload/20120112/DynamicLayers.zip

You have to consume a MPK with existing layers and, as far as i know, you can only change the existing layers to point to a new data point. I added two buttons. The first one will update the Europe map with a source from the file geodatabase and change the rendering. The second map will replace the Europe map with a map of Acadia National Park (of the coast of Maine, USA). As you can also see, the legend is not updated when doing these changes.

Can you shed some light on how to achieve my initial idea within Runtime? Is this possible?

Thank you!
0 Kudos
MichaelBranscomb
Esri Frequent Contributor
Hi,

I believe the dynamic layers functionality can be used in the way you are asking. The code in the original example below uses the CreateDynamicLayerInfosFromLayerInfos() method which returns a DynamicLayerInfoCollection containing the existing layers in the map allowing you to make modifications to those. You can continue with that approach if you would like to maintain/modify the existing layers but also add new DynamicLayerInfo objects to that DynamicLayerInfoCollection to add brand new feature classes. Or alternatively you can simply create a new DynamicLayerInfoCollection and fill it with any number of DynamicLayerInfo objects (one for each feature class you would like to add). Note that the Geodatabase workspace must always be registered with the LocalMapService prior to starting the service, and obviously also that LocalMapService will need to be started from an MPK. This MPK will need to contain at least one layer - and for best performance the coordinate system of the MPK should match that of the new feature classes you are adding (although LocalMapServices will perform reprojection on-the-fly).

Remember - no changes are made to the actual underlying LocalMapService or the MPK on which it is based. It is the client layer (ArcGISLocalDynamicMapServiceLayer) which manages the persistence of the new feature layers and renderer you have defined. The same functionality is also available in ArcGIS for Server 10.1 but in that case the workspaces must be accessible from the server and must be registered with the server via the ArcGIS Server Manager web app (or ArcCatalog). The registering of workspaces via the client API is only supported for LocalMapServices.

Regarding the legend control - we'll investigate.

Cheers

Mike
0 Kudos
BKuiper
Occasional Contributor III
Hi Mike,

I tried that yesterday, using the DynamicLayerInfos object, but failed. But looking at it again, I got it working!

One of the tricks is also to assign the correct renderer to the correct layer. Layers can have different renderers by specifying the LayerId on the LayerDrawOptions object.

here is some mocked-up code to prove the concept:

        private void AddLayerButton_Click(object sender, RoutedEventArgs e)
        {
            var layerInfos = this.dynamicMapServiceLayer.CreateDynamicLayerInfosFromLayerInfos();

            int highestId = -1;

            if (layerInfos.Count != 0)
            {
                highestId = layerInfos.Max(x => x.ID);
            }

            var newInfo = new DynamicLayerInfo()
            {
                ID = highestId + 1,
                Source = new LayerDataSource()
                {
                    DataSource = new TableDataSource()
                    {
                        DataSourceName = "parkboundary",
                        WorkspaceID = "DemoGDB"
                    }
                },
            };

            layerInfos.Add(newInfo);

            this.dynamicMapServiceLayer.DynamicLayerInfos = layerInfos;

            LayerDrawingOptions layerDrawOpt = new LayerDrawingOptions();
            layerDrawOpt.LayerID = highestId + 1;
            layerDrawOpt.Renderer = new SimpleRenderer() { Symbol = GetSimpleFillSymbol(Colors.Blue, Colors.Blue, 1) };

            this.dynamicMapServiceLayer.LayerDrawingOptions = new LayerDrawingOptionsCollection() { layerDrawOpt };

            this.dynamicMapServiceLayer.Refresh();
        }


this will add a new (blue) layer to the existing list of layers.

The legend problem remains, but you are already investigating this.
0 Kudos
BKuiper
Occasional Contributor III
So I guess the problem with the legend is related to the fact that the new layer is not exposed on the REST endpoint of the ArcGIS Runtime. I remember from ArcGIS 10 that the legend uses the REST endpoint to generate its content.

If the layer would be exposed on the REST endpoint you would be able to consume it as a Feature Layer, the next step I want to achieve. Is this correct? Would you be able to create a "Dynamic" Feature Layer using this technology ?

Thanks again for your time and efforts on this forum!
0 Kudos
BKuiper
Occasional Contributor III
Hi Mike,

Am I right about consuming the Dynamic Layers as Features Layers? Will this be possible when the Layers will be exposed on the REST endpoint ? This should also fix the problem with the legen is my guess.

Btw, are you visiting the DevSummit ? Looking forward in meeting the ArcGIS Runtime team!
0 Kudos