Now that we've learned some important concepts regarding 3D, let's take our app and make it so the user can control the scene using an intuitive UI, while at the same time extending our understanding of MVVM by adding another ViewModel
class to our app that makes the use of a reusable user control:
Json.NET
, copy the code files from the previous project, and then update all references and the using
statements.MainWindow.xaml
of Chapter4
to the new MainWindow.xaml
and update the using
statements. Make sure the app runs as with the previous exercise. If you have any issues with the app not binding to the ViewModel
class, check the binding in the XAML code and update them, like in this example:<TextBox Name="SearchTextBox" Text="{Binding SearchText, Source={StaticResource Locator}}"></TextBox>
ViewModel
class. Be sure to select MvvmViewModel (WPF). Call it CameraViewModel.cs
and place it in the ViewModels
folder. Add the following code to it:using GalaSoft.MvvmLight; using GalaSoft.MvvmLight.Messaging; using Esri.ArcGISRuntime.Geometry; using Esri.ArcGISRuntime.Controls; namespace Chapter4a.ViewModels { /// <summary> /// This class contains properties that a View can data bind to. /// <para> /// See http://www.galasoft.ch/mvvm /// </para> /// </summary> public class CameraViewModel : ViewModelBase { Esri.ArcGISRuntime.Controls.SceneView sceneView = null; double heading = 0.0; double pitch = 0.0; double elevation = 11000000; double latitude = 0.0; double longitude = 0.0; /// <summary> /// Initializes a new instance of the LocationViewModel class. /// </summary> public CameraViewModel() { Messenger.Default.Register<SceneView>(this, (sceneView) => { this.sceneView = sceneView; }); } public double Heading { get { return this.heading; } set { this.heading = value; RaisePropertyChanged("Heading"); MapPoint myLocation = new MapPoint(this.longitude, this.latitude, this.elevation, SpatialReferences.Wgs84); Camera myCamera = new Camera(myLocation, value, this.pitch); this.sceneView.SetView(myCamera); } } public double Pitch { get { return this.pitch; } set { this.pitch = value; RaisePropertyChanged("Pitch"); MapPoint myLocation = new MapPoint(this.longitude, this.latitude, this.elevation, SpatialReferences.Wgs84); Camera myCamera = new Camera(myLocation, this.heading, value); this.sceneView.SetView(myCamera); } } public double Elevation { get { return this.elevation; } set { this.elevation = value; RaisePropertyChanged("Elevation"); MapPoint myLocation = new MapPoint(this.longitude, this.latitude, value, SpatialReferences.Wgs84); Camera myCamera = new Camera(myLocation, this.heading, this.pitch); this.sceneView.SetView(myCamera); } } public double Latitude { get { return this.latitude; } set { this.latitude = value; RaisePropertyChanged("Latitude"); MapPoint myLocation = new MapPoint(this.longitude, value, this.elevation, SpatialReferences.Wgs84); Camera myCamera = new Camera(myLocation, this.heading, this.pitch); this.sceneView.SetView(myCamera); } } public double Longitude { get { return this.longitude; } set { this.longitude = value; RaisePropertyChanged("Longitude"); MapPoint myLocation = new MapPoint(value, this.latitude, this.elevation, SpatialReferences.Wgs84); Camera myCamera = new Camera(myLocation, this.heading, this.pitch); this.sceneView.SetView(myCamera); } } } }
ViewModelLocator
, register the new ViewModel
class just like MainViewModel
is registered:SimpleIoc.Default.Register<CameraViewModel>();
ViewModelLocator
like this:public CameraViewModel MainViewModel { get { return ServiceLocator.Current.GetInstance<CameraViewModel>(); } }
UserControls
. Name the UserControl
folder's CameraUserControl.xaml
file like this:UserControl
:<UserControl x:Class="Chapter4a.UserControls.CameraUserControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" xmlns:vm="clr-namespace:Chapter4a.ViewModels" mc:Ignorable="d" d:DesignHeight="151" d:DesignWidth="240"> <UserControl.Resources> <vm:CameraViewModel x:Key="Locator" d:IsDataSource="True"/> </UserControl.Resources> <Grid DataContext="{Binding CameraViewModel, Source={StaticResource Locator}}"> <StackPanel Orientation="Vertical" Margin="10"> <!-- Slider for the Heading (in Degrees) of the Camera. --> <StackPanel Orientation="Horizontal"> <Label Content="Heading: " Foreground="Green" FontWeight="Bold" Width="70" /> <Slider x:Name="Slider_Heading" Width="100" AutoToolTipPlacement="BottomRight" Minimum="0" Maximum="360" TickFrequency="1" Value="{Binding Heading, Source={StaticResource Locator}}"/> <TextBlock x:Name="Heading_Value" VerticalAlignment="Center" FontWeight="Bold" Foreground="Green" Text="{Binding Heading, StringFormat='N0', Source={StaticResource Locator}}"/> </StackPanel> <!-- Slider for the Pitch (in Degrees) of the Camera. --> <StackPanel Orientation="Horizontal"> <Label Content="Pitch: " Foreground="Green" FontWeight="Bold" Width="70" /> <Slider x:Name="Slider_Pitch" Width="100" AutoToolTipPlacement="BottomRight" Minimum="0" Maximum="180" TickFrequency="1" Value="{Binding Pitch, Source={StaticResource Locator}}"/> <TextBlock x:Name="Pitch_Value" VerticalAlignment="Center" FontWeight="Bold" Foreground="Green" Text="{Binding Pitch, StringFormat='N0', Source={StaticResource Locator}}"/> </StackPanel> <!-- Slider for the Z value (aka. Elevation in Meters) of the Camera's Location. --> <StackPanel Orientation="Horizontal"> <Label Content="Elevation: " Foreground="Green" FontWeight="Bold" Width="70" /> <Slider x:Name="Slider_Z" Width="200" AutoToolTipPlacement="BottomRight" Minimum="0" Maximum="15000000" TickFrequency="1" Value="{Binding Elevation, StringFormat='N0', Source={StaticResource Locator}}"/> <TextBlock x:Name="Z_Value" VerticalAlignment="Center" Foreground="Green" Text="{Binding Elevation, StringFormat='N0', Source={StaticResource Locator}}"/> </StackPanel> <!-- Slider for the X value (aka. Longitude in Decimal Degrees) of the Camera's Location. --> <StackPanel Orientation="Horizontal"> <Label Content="Longitude: " Foreground="Green" FontWeight="Bold" Width="70" /> <Slider x:Name="Slider_X" Width="200" AutoToolTipPlacement="BottomRight" AutoToolTipPrecision="2" Minimum="-180" Maximum="180" TickFrequency="0.01" Value="{Binding Longitude, Source={StaticResource Locator}}" /> <TextBlock x:Name="X_Value" Foreground="Green" FontWeight="Bold" VerticalAlignment="Center" Text="{Binding Longitude,StringFormat='N2', Source={StaticResource Locator}}"/> </StackPanel> <!-- Slider for the Y value (aka. Latitude in Decimal Degrees) of the Camera's Location. --> <StackPanel Orientation="Horizontal"> <Label Content="Latitude: " Foreground="Green" FontWeight="Bold" Width="70" /> <Slider x:Name="Slider_Y" Width="200" AutoToolTipPlacement="BottomRight" AutoToolTipPrecision="2" Minimum="-90" Maximum="90" TickFrequency="0.01" Value="{Binding Latitude, Source={StaticResource Locator}}"/> <TextBlock x:Name="Y_Value" Foreground="Green" FontWeight="Bold" VerticalAlignment="Center" Text="{Binding Latitude, StringFormat='N2',Source={StaticResource Locator}}"/> </StackPanel> </StackPanel> </Grid> </UserControl>
MainWindow.xaml
, add the following using
statement:xmlns:uc="clr-namespace:Chapter4a.UserControls"
SceneView
tag, add this line:<uc:CameraUserControl HorizontalAlignment="Right" Margin="0, 50"> </uc:CameraUserControl>
Several changes were made to this app to add these new capabilities. We created a new ViewModel
class and added it to the MVVM Light registry of ViewModel
classes. This required adding a property to ViewModel Locator so that the View
class could find it. We then populated the new ViewModel
class (CameraViewModel
) with several properties so that the user control could bind to it. We then created a user control and set its DataContext
to ViewModel
so that it knows which one to use via the Locator. In this case, it's set to CameraViewModel
.
In the user control, we bound each slider to a property on the ViewModel
class. For example, the pitch slider was bound to the Pitch
property so that when a user changes the pitch, it adjusts the globe's pitch. If you look at the pitch slider, you'll note that it has a range from 0 to 180, which is the range it should have. The latitude slider also has a range from -90 to 90 degrees, as it should be. The other sliders also have appropriate ranges. In the ViewModel
class, the properties are updated every time the user changes a slider because RaisePropertyChanged
is called for the respective property. Also, the camera is updated with the new position from a newly created MapPoint
geometry. (We'll talk more about MapPoint
and other geometries in the next chapter.) Lastly, the SceneView
property is updated with this.sceneView.SetView(myCamera)
.
The really important new capability that we have created here is a reusable UserControl
folder that we could put into another project if we needed it. All that would be necessary is we just copy the ViewModel
and UserControl
properties over into the project and update its Locator, and it should work perfectly fine. An even better solution would be to place UserControl
and ViewModel
into its own reusable class library, and then add it to any project. Of course, before this new user control could really be used, it would need to take into account the mouse movements.