From 4ea0bcab05268580a022c780f7508ce5986170cb Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Thu, 12 Oct 2023 12:03:22 -0700 Subject: [PATCH 1/7] Adds a GeoView controller to help with common MVVM challenges --- .../Toolkit.SampleApp.Maui/MauiProgram.cs | 5 +- .../SampleDatasource.cs | 2 +- .../Samples/GeoViewControllerSample.xaml | 25 ++ .../Samples/GeoViewControllerSample.xaml.cs | 40 +++ .../Toolkit.SampleApp.Maui.csproj | 11 + .../Toolkit.SampleApp.UWP/SampleDatasource.cs | 2 +- .../GeoViewControllerSample.xaml | 19 ++ .../GeoViewControllerSample.xaml.cs | 11 + .../GeoViewControllerSampleVM.cs | 32 +++ .../Toolkit.Samples.UWP.csproj | 8 + .../Toolkit.SampleApp.WPF/SampleDatasource.cs | 2 +- .../GeoViewControllerSample.xaml | 25 ++ .../GeoViewControllerSample.xaml.cs | 12 + .../GeoViewControllerSampleVM.cs | 34 +++ .../Toolkit.SampleApp.WPF.csproj | 5 + src/Toolkit/Toolkit/GeoViewController.cs | 264 ++++++++++++++++++ 16 files changed, 492 insertions(+), 5 deletions(-) create mode 100644 src/Samples/Toolkit.SampleApp.Maui/Samples/GeoViewControllerSample.xaml create mode 100644 src/Samples/Toolkit.SampleApp.Maui/Samples/GeoViewControllerSample.xaml.cs create mode 100644 src/Samples/Toolkit.SampleApp.UWP/Samples/GeoViewController/GeoViewControllerSample.xaml create mode 100644 src/Samples/Toolkit.SampleApp.UWP/Samples/GeoViewController/GeoViewControllerSample.xaml.cs create mode 100644 src/Samples/Toolkit.SampleApp.UWP/Samples/GeoViewController/GeoViewControllerSampleVM.cs create mode 100644 src/Samples/Toolkit.SampleApp.WPF/Samples/GeoViewController/GeoViewControllerSample.xaml create mode 100644 src/Samples/Toolkit.SampleApp.WPF/Samples/GeoViewController/GeoViewControllerSample.xaml.cs create mode 100644 src/Samples/Toolkit.SampleApp.WPF/Samples/GeoViewController/GeoViewControllerSampleVM.cs create mode 100644 src/Toolkit/Toolkit/GeoViewController.cs diff --git a/src/Samples/Toolkit.SampleApp.Maui/MauiProgram.cs b/src/Samples/Toolkit.SampleApp.Maui/MauiProgram.cs index 5aa10604a..1e1307418 100644 --- a/src/Samples/Toolkit.SampleApp.Maui/MauiProgram.cs +++ b/src/Samples/Toolkit.SampleApp.Maui/MauiProgram.cs @@ -1,4 +1,5 @@ -using Esri.ArcGISRuntime.Maui; +using CommunityToolkit.Maui; +using Esri.ArcGISRuntime.Maui; using Esri.ArcGISRuntime.Toolkit.Maui; namespace Toolkit.SampleApp.Maui; @@ -14,7 +15,7 @@ public static MauiApp CreateMauiApp() { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); - }).UseArcGISRuntime().UseArcGISToolkit(); + }).UseArcGISRuntime().UseArcGISToolkit().UseMauiCommunityToolkit(); return builder.Build(); } diff --git a/src/Samples/Toolkit.SampleApp.Maui/SampleDatasource.cs b/src/Samples/Toolkit.SampleApp.Maui/SampleDatasource.cs index afdd9694f..31686e134 100644 --- a/src/Samples/Toolkit.SampleApp.Maui/SampleDatasource.cs +++ b/src/Samples/Toolkit.SampleApp.Maui/SampleDatasource.cs @@ -51,7 +51,7 @@ public Sample(Type page) if (string.IsNullOrEmpty(Name)) { //Deduce name from type name - Name = Regex.Replace(Page.Name, @"((?<=\p{Ll})\p{Lu})|((?!\A)\p{Lu}(?>\p{Ll}))", " $0").Replace("Arc GIS", "ArcGIS"); + Name = Regex.Replace(Page.Name, @"((?<=\p{Ll})\p{Lu})|((?!\A)\p{Lu}(?>\p{Ll}))", " $0").Replace("Arc GIS", "ArcGIS").Replace("Geo View", "GeoView"); if (Name.EndsWith("Sample")) Name = Name.Substring(0, Name.Length - 6); } diff --git a/src/Samples/Toolkit.SampleApp.Maui/Samples/GeoViewControllerSample.xaml b/src/Samples/Toolkit.SampleApp.Maui/Samples/GeoViewControllerSample.xaml new file mode 100644 index 000000000..300a7c0ae --- /dev/null +++ b/src/Samples/Toolkit.SampleApp.Maui/Samples/GeoViewControllerSample.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Samples/Toolkit.SampleApp.Maui/Samples/GeoViewControllerSample.xaml.cs b/src/Samples/Toolkit.SampleApp.Maui/Samples/GeoViewControllerSample.xaml.cs new file mode 100644 index 000000000..7195d1a17 --- /dev/null +++ b/src/Samples/Toolkit.SampleApp.Maui/Samples/GeoViewControllerSample.xaml.cs @@ -0,0 +1,40 @@ +using CommunityToolkit.Mvvm.Input; +using Esri.ArcGISRuntime.Data; +using Esri.ArcGISRuntime.Geometry; +using Esri.ArcGISRuntime.Mapping; +using Esri.ArcGISRuntime.Maui; +using Esri.ArcGISRuntime.Toolkit.Maui; + +namespace Toolkit.SampleApp.Maui.Samples; + +public partial class GeoViewControllerSample : ContentPage +{ + public GeoViewControllerSample() + { + InitializeComponent(); + } +} + +public partial class GeoViewControllerSampleVM +{ + public Map Map { get; } = new Map(new Uri("https://www.arcgis.com/home/item.html?id=9f3a674e998f461580006e626611f9ad")); + + public GeoViewController Controller { get; } = new GeoViewController(); + + [RelayCommand] + public async Task OnGeoViewTapped(GeoViewInputEventArgs eventArgs) => await Identify(eventArgs.Position, eventArgs.Location); + + public async Task Identify(Point location, MapPoint? mapLocation) + { + Controller.DismissCallout(); + var result = await Controller.IdentifyLayersAsync(location, 10); + if (result.FirstOrDefault()?.GeoElements?.FirstOrDefault() is GeoElement element) + { + Controller.ShowCalloutForGeoElement(element, location, new Esri.ArcGISRuntime.UI.CalloutDefinition(element)); + } + else if (mapLocation is not null) + { + Controller.ShowCalloutAt(mapLocation, new Esri.ArcGISRuntime.UI.CalloutDefinition("No features found")); + } + } +} \ No newline at end of file diff --git a/src/Samples/Toolkit.SampleApp.Maui/Toolkit.SampleApp.Maui.csproj b/src/Samples/Toolkit.SampleApp.Maui/Toolkit.SampleApp.Maui.csproj index d09653fc2..c88346068 100644 --- a/src/Samples/Toolkit.SampleApp.Maui/Toolkit.SampleApp.Maui.csproj +++ b/src/Samples/Toolkit.SampleApp.Maui/Toolkit.SampleApp.Maui.csproj @@ -53,6 +53,17 @@ + + + + + + + + MSBuild:Compile + + + diff --git a/src/Samples/Toolkit.SampleApp.UWP/SampleDatasource.cs b/src/Samples/Toolkit.SampleApp.UWP/SampleDatasource.cs index bfd627e12..439c1c987 100644 --- a/src/Samples/Toolkit.SampleApp.UWP/SampleDatasource.cs +++ b/src/Samples/Toolkit.SampleApp.UWP/SampleDatasource.cs @@ -40,7 +40,7 @@ where t.GetTypeInfo().IsSubclassOf(typeof(Page)) && t.FullName.Contains(".Sample } if (string.IsNullOrEmpty(sample.Name)) { - sample.Name = Regex.Replace(sample.Page.Name, @"((?<=\p{Ll})\p{Lu})|((?!\A)\p{Lu}(?>\p{Ll}))", " $0").Replace("Arc GIS", "ArcGIS"); + sample.Name = Regex.Replace(sample.Page.Name, @"((?<=\p{Ll})\p{Lu})|((?!\A)\p{Lu}(?>\p{Ll}))", " $0").Replace("Arc GIS", "ArcGIS").Replace("Geo View", "GeoView").Replace("Geo View", "GeoView"); if (sample.Name.EndsWith("Sample")) sample.Name = sample.Name.Substring(0, sample.Name.Length - 6); } diff --git a/src/Samples/Toolkit.SampleApp.UWP/Samples/GeoViewController/GeoViewControllerSample.xaml b/src/Samples/Toolkit.SampleApp.UWP/Samples/GeoViewController/GeoViewControllerSample.xaml new file mode 100644 index 000000000..da16f7f9e --- /dev/null +++ b/src/Samples/Toolkit.SampleApp.UWP/Samples/GeoViewController/GeoViewControllerSample.xaml @@ -0,0 +1,19 @@ + + + + + + diff --git a/src/Samples/Toolkit.SampleApp.UWP/Samples/GeoViewController/GeoViewControllerSample.xaml.cs b/src/Samples/Toolkit.SampleApp.UWP/Samples/GeoViewController/GeoViewControllerSample.xaml.cs new file mode 100644 index 000000000..fecbb5519 --- /dev/null +++ b/src/Samples/Toolkit.SampleApp.UWP/Samples/GeoViewController/GeoViewControllerSample.xaml.cs @@ -0,0 +1,11 @@ +namespace Esri.ArcGISRuntime.Toolkit.SampleApp.Samples.GeoViewController +{ + public sealed partial class GeoViewControllerSample : Page + { + public GeoViewControllerSample() + { + this.InitializeComponent(); + } + public GeoViewControllerSampleVM VM { get; } = new GeoViewControllerSampleVM(); + } +} diff --git a/src/Samples/Toolkit.SampleApp.UWP/Samples/GeoViewController/GeoViewControllerSampleVM.cs b/src/Samples/Toolkit.SampleApp.UWP/Samples/GeoViewController/GeoViewControllerSampleVM.cs new file mode 100644 index 000000000..0fcc3bae9 --- /dev/null +++ b/src/Samples/Toolkit.SampleApp.UWP/Samples/GeoViewController/GeoViewControllerSampleVM.cs @@ -0,0 +1,32 @@ +using System; +using System.Linq; +using Esri.ArcGISRuntime.Data; +using Esri.ArcGISRuntime.Geometry; +using Esri.ArcGISRuntime.Mapping; +using Esri.ArcGISRuntime.UI.Controls; +using Windows.Foundation; + +namespace Esri.ArcGISRuntime.Toolkit.SampleApp.Samples.GeoViewController; + +public class GeoViewControllerSampleVM +{ + public Map Map { get; } = new Map(new Uri("https://www.arcgis.com/home/item.html?id=9f3a674e998f461580006e626611f9ad")); + + public UI.GeoViewController Controller { get; } = new UI.GeoViewController(); + + public void OnGeoViewTapped(object sender, GeoViewInputEventArgs eventArgs) => Identify(eventArgs.Position, eventArgs.Location); + + public async void Identify(Point location, MapPoint? mapLocation) + { + Controller.DismissCallout(); + var result = await Controller.IdentifyLayersAsync(location, 10); + if (result.FirstOrDefault()?.GeoElements?.FirstOrDefault() is GeoElement element) + { + Controller.ShowCalloutForGeoElement(element, location, new Esri.ArcGISRuntime.UI.CalloutDefinition(element)); + } + else if (mapLocation is not null) + { + Controller.ShowCalloutAt(mapLocation, new Esri.ArcGISRuntime.UI.CalloutDefinition("No features found")); + } + } +} \ No newline at end of file diff --git a/src/Samples/Toolkit.SampleApp.UWP/Toolkit.Samples.UWP.csproj b/src/Samples/Toolkit.SampleApp.UWP/Toolkit.Samples.UWP.csproj index 80448cef1..7a2959650 100644 --- a/src/Samples/Toolkit.SampleApp.UWP/Toolkit.Samples.UWP.csproj +++ b/src/Samples/Toolkit.SampleApp.UWP/Toolkit.Samples.UWP.csproj @@ -134,6 +134,10 @@ FloorFilterSample.xaml + + GeoViewControllerSample.xaml + + TimeSliderSample.xaml @@ -227,6 +231,10 @@ MSBuild:Compile Designer + + Designer + MSBuild:Compile + MSBuild:Compile Designer diff --git a/src/Samples/Toolkit.SampleApp.WPF/SampleDatasource.cs b/src/Samples/Toolkit.SampleApp.WPF/SampleDatasource.cs index 780bf1a1e..402ae2ec5 100644 --- a/src/Samples/Toolkit.SampleApp.WPF/SampleDatasource.cs +++ b/src/Samples/Toolkit.SampleApp.WPF/SampleDatasource.cs @@ -43,7 +43,7 @@ where t.GetTypeInfo().IsSubclassOf(typeof(UserControl)) && t.FullName.Contains(" sample.Name = sample.Page.Name; if (sample.Name.EndsWith("Sample")) sample.Name = sample.Name.Substring(0, sample.Name.Length - 6); - sample.Name = Regex.Replace(sample.Name, @"((?<=\p{Ll})\p{Lu})|((?!\A)\p{Lu}(?>\p{Ll}))", " $0").Replace("Arc GIS", "ArcGIS"); + sample.Name = Regex.Replace(sample.Name, @"((?<=\p{Ll})\p{Lu})|((?!\A)\p{Lu}(?>\p{Ll}))", " $0").Replace("Arc GIS", "ArcGIS").Replace("Geo View", "GeoView"); } if (string.IsNullOrEmpty(sample.Category)) { diff --git a/src/Samples/Toolkit.SampleApp.WPF/Samples/GeoViewController/GeoViewControllerSample.xaml b/src/Samples/Toolkit.SampleApp.WPF/Samples/GeoViewController/GeoViewControllerSample.xaml new file mode 100644 index 000000000..0033f813d --- /dev/null +++ b/src/Samples/Toolkit.SampleApp.WPF/Samples/GeoViewController/GeoViewControllerSample.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + diff --git a/src/Samples/Toolkit.SampleApp.WPF/Samples/GeoViewController/GeoViewControllerSample.xaml.cs b/src/Samples/Toolkit.SampleApp.WPF/Samples/GeoViewController/GeoViewControllerSample.xaml.cs new file mode 100644 index 000000000..daf8cc246 --- /dev/null +++ b/src/Samples/Toolkit.SampleApp.WPF/Samples/GeoViewController/GeoViewControllerSample.xaml.cs @@ -0,0 +1,12 @@ +using System.Windows.Controls; + +namespace Esri.ArcGISRuntime.Toolkit.Samples.GeoViewController +{ + public partial class GeoViewControllerSample : UserControl + { + public GeoViewControllerSample() + { + InitializeComponent(); + } + } +} diff --git a/src/Samples/Toolkit.SampleApp.WPF/Samples/GeoViewController/GeoViewControllerSampleVM.cs b/src/Samples/Toolkit.SampleApp.WPF/Samples/GeoViewController/GeoViewControllerSampleVM.cs new file mode 100644 index 000000000..0329ecb03 --- /dev/null +++ b/src/Samples/Toolkit.SampleApp.WPF/Samples/GeoViewController/GeoViewControllerSampleVM.cs @@ -0,0 +1,34 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using CommunityToolkit.Mvvm.Input; +using Esri.ArcGISRuntime.Data; +using Esri.ArcGISRuntime.Geometry; +using Esri.ArcGISRuntime.Mapping; +using Esri.ArcGISRuntime.UI.Controls; + +namespace Esri.ArcGISRuntime.Toolkit.Samples.GeoViewController; +public partial class GeoViewControllerSampleVM +{ + public Map Map { get; } = new Map(new Uri("https://www.arcgis.com/home/item.html?id=9f3a674e998f461580006e626611f9ad")); + + public UI.GeoViewController Controller { get; } = new UI.GeoViewController(); + + [RelayCommand] + public async Task OnGeoViewTapped(GeoViewInputEventArgs eventArgs) => await Identify(eventArgs.Position, eventArgs.Location); + + public async Task Identify(Point location, MapPoint? mapLocation) + { + Controller.DismissCallout(); + var result = await Controller.IdentifyLayersAsync(location, 10); + if (result.FirstOrDefault()?.GeoElements?.FirstOrDefault() is GeoElement element) + { + Controller.ShowCalloutForGeoElement(element, location, new Esri.ArcGISRuntime.UI.CalloutDefinition(element)); + } + else if (mapLocation is not null) + { + Controller.ShowCalloutAt(mapLocation, new Esri.ArcGISRuntime.UI.CalloutDefinition("No features found")); + } + } +} \ No newline at end of file diff --git a/src/Samples/Toolkit.SampleApp.WPF/Toolkit.SampleApp.WPF.csproj b/src/Samples/Toolkit.SampleApp.WPF/Toolkit.SampleApp.WPF.csproj index 1871cd0fd..972c56a7c 100644 --- a/src/Samples/Toolkit.SampleApp.WPF/Toolkit.SampleApp.WPF.csproj +++ b/src/Samples/Toolkit.SampleApp.WPF/Toolkit.SampleApp.WPF.csproj @@ -23,6 +23,11 @@ + + + + + diff --git a/src/Toolkit/Toolkit/GeoViewController.cs b/src/Toolkit/Toolkit/GeoViewController.cs new file mode 100644 index 000000000..ef8ece674 --- /dev/null +++ b/src/Toolkit/Toolkit/GeoViewController.cs @@ -0,0 +1,264 @@ +#nullable enable +using Esri.ArcGISRuntime.Data; +using Esri.ArcGISRuntime.Mapping; +using Esri.ArcGISRuntime.Toolkit.Internal; +using Esri.ArcGISRuntime.UI; +using System.Diagnostics; +#if WINUI +using Point = Windows.Foundation.Point; +using LoadedEventArgs = Microsoft.UI.Xaml.RoutedEventArgs; +#elif WINDOWS_UWP +using Point = Windows.Foundation.Point; +using LoadedEventArgs = Windows.UI.Xaml.RoutedEventArgs; +#else +using LoadedEventArgs = System.EventArgs; +#endif +#if WINUI +using GeoViewDPType = Microsoft.UI.Xaml.DependencyObject; +#elif !MAUI +using GeoViewDPType = Esri.ArcGISRuntime.UI.Controls.GeoView; +#endif + +#if MAUI +namespace Esri.ArcGISRuntime.Toolkit.Maui +#else +namespace Esri.ArcGISRuntime.Toolkit.UI +#endif +{ + /// + /// Helper class for separating GeoView and ViewModel in an MVVM pattern, while allowing operations on the view from the ViewModel. + /// + public class GeoViewController + { + private WeakReference? _geoViewWeak; + private WeakEventListener? _loadedListener; + private WeakEventListener? _unloadedListener; + + /// + /// Gets a reference to the GeoView this controller is currently connected to. + /// + protected GeoView? ConnectedView => _geoViewWeak?.TryGetTarget(out var view) == true ? view : null; + + private void AttachToGeoView(GeoView mv) + { + var connectedView = ConnectedView; + if (connectedView == mv) + { + return; + } + if (connectedView != null) + { + Trace.WriteLine("Warning: GeoViewController already connected to a GeoView - Moving GeoViewController to a new GeoView", "ArcGIS Maps SDK Toolkit"); + DetachFromGeoView(connectedView); + connectedView = null; + } + // All event listeners must be weak to avoid holding on to the GeoView if the GeoViewController stays alive + _geoViewWeak = new WeakReference(mv); + _loadedListener = new WeakEventListener(this, mv) + { + OnEventAction = static (instance, source, eventArgs) => instance.GeoView_Loaded((GeoView)source!), + OnDetachAction = static (instance, source, weakEventListener) => source.Loaded -= weakEventListener.OnEvent, + }; + mv.Loaded += _loadedListener.OnEvent; + + _unloadedListener = new WeakEventListener(this, mv) + { + OnEventAction = static (instance, source, eventArgs) => instance.GeoView_Unloaded((GeoView)source!), + OnDetachAction = static (instance, source, weakEventListener) => source.Unloaded -= weakEventListener.OnEvent, + }; + mv.Loaded += _loadedListener.OnEvent; + mv.Unloaded += _unloadedListener.OnEvent; + OnGeoViewAttached(mv); + if (mv.IsLoaded) + GeoView_Loaded(mv); + } + + private void DetachFromGeoView(GeoView mv) + { + var connectedView = ConnectedView; + if (connectedView != null && connectedView == mv) + { + _loadedListener?.Detach(); + _unloadedListener?.Detach(); + if (connectedView.IsLoaded) + GeoView_Unloaded(mv); + OnGeoViewDetached(connectedView); + _geoViewWeak = null; + } + } + + /// + /// Raised when the has been attached to a . + /// + /// + protected virtual void OnGeoViewAttached(GeoView geoView) + { + } + + /// + /// Raised when the has been detached from a . + /// + /// + protected virtual void OnGeoViewDetached(GeoView geoView) + { + } + + /// + /// Raised when the attached loads into the active view. + /// + /// GeoView that was loaded + protected virtual void OnGeoViewLoaded(GeoView geoView) + { + } + + /// + /// Raised when the attached unloads from the active view. + /// + /// GeoView that was unloaded + protected virtual void OnGeoViewUnloaded(GeoView geoView) + { + } + + private void GeoView_Loaded(GeoView sender) + { + Debug.Assert(sender == ConnectedView && ConnectedView is not null); + OnGeoViewLoaded(sender); + } + + private void GeoView_Unloaded(GeoView sender) + { + Debug.Assert(sender == ConnectedView && ConnectedView is not null); + OnGeoViewUnloaded(sender); + } + + #region Viewpoints + + /// + public Viewpoint? GetCurrentViewpoint(ViewpointType viewpointType) => ConnectedView?.GetCurrentViewpoint(viewpointType); + + /// + public Task SetViewpointAsync(Viewpoint viewpoint) => ConnectedView?.SetViewpointAsync(viewpoint) ?? Task.FromResult(false); + + /// + public Task SetViewpointAsync(Viewpoint viewpoint, TimeSpan duration) => ConnectedView?.SetViewpointAsync(viewpoint, duration) ?? Task.FromResult(false); + + /// + public void SetViewpoint(Viewpoint viewpoint) => ConnectedView?.SetViewpoint(viewpoint); + + #endregion Viewpoints + + #region Identify + + /// + public Task> IdentifyLayersAsync(Point screenPoint, double tolerance, bool returnPopupsOnly = false, CancellationToken cancellationToken = default) => + ConnectedView?.IdentifyLayersAsync(screenPoint, tolerance, returnPopupsOnly, cancellationToken) ?? + Task.FromResult>(new List().AsReadOnly()); + + /// + public Task IdentifyLayerAsync(Layer layer, Point screenPoint, double tolerance, bool returnPopupsOnly = false, CancellationToken cancellationToken = default) => + ConnectedView?.IdentifyLayerAsync(layer, screenPoint, tolerance, returnPopupsOnly, cancellationToken) ?? + Task.FromResult(null!); + + /// + public Task> IdentifyGraphicsOverlaysAsync(Point screenPoint, double tolerance, bool returnPopupsOnly = false, long maximumResultsPerOverlay = 1) => + ConnectedView?.IdentifyGraphicsOverlaysAsync(screenPoint, tolerance, returnPopupsOnly, maximumResultsPerOverlay) ?? + Task.FromResult>(new List().AsReadOnly()); + + /// + public Task IdentifyGraphicsOverlayAsync(GraphicsOverlay overlay, Point screenPoint, double tolerance, bool returnPopupsOnly = false, long maximumResults = 1) => + ConnectedView?.IdentifyGraphicsOverlayAsync(overlay, screenPoint, tolerance, returnPopupsOnly, maximumResults) ?? + Task.FromResult(null!); + #endregion Identify + + #region Callouts + + /// + public void DismissCallout() => ConnectedView?.DismissCallout(); + + /// + public void ShowCalloutAt(Esri.ArcGISRuntime.Geometry.MapPoint location, CalloutDefinition definition) => ConnectedView?.ShowCalloutAt(location, definition); + + /// + public void ShowCalloutForGeoElement(GeoElement element, Point tapPosition, CalloutDefinition definition) => ConnectedView?.ShowCalloutForGeoElement(element, tapPosition, definition); + #endregion Callouts + +#if MAUI + /// + /// Identifies the attached property. + /// + public static BindableProperty GeoViewControllerProperty = + BindableProperty.CreateAttached( + "GeoViewController", + typeof(GeoViewController), + typeof(GeoViewController), + null, + propertyChanged: OnGeoViewControllerChanged + ); + + private static void OnGeoViewControllerChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is GeoView geoView) + { + if (oldValue is GeoViewController controllerOld) + { + controllerOld.DetachFromGeoView(geoView); + } + + if (newValue is GeoViewController controllerNew) + { + controllerNew.AttachToGeoView(geoView); + } + } + else + { + throw new InvalidOperationException("This property must be attached to a GeoView."); + } + } +#else + /// + /// Identifies the attached property. + /// + public static readonly DependencyProperty GeoViewControllerProperty = + DependencyProperty.RegisterAttached( + "GeoViewController", + typeof(GeoViewController), + typeof(GeoViewController), + new PropertyMetadata(null, OnGeoViewControllerChanged) + ); + + /// + /// Gets the value of the XAML attached property from the specified . + /// + /// The from which to read the property value. + /// The value of the XAML attached property on the target element. + public static GeoViewController? GetGeoViewController(GeoViewDPType geoView) => geoView.GetValue(GeoViewControllerProperty) as GeoViewController; + + /// + /// Sets the value of the XAML attached property on the specified object. + /// + /// The target on which to set the XAML attached property. + /// The property value to set. + public static void SetGeoViewController(GeoViewDPType geoView, GeoViewController? value) => geoView.SetValue(GeoViewControllerProperty, value); + + private static void OnGeoViewControllerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is GeoView geoView) + { + if (e.OldValue is GeoViewController controllerOld) + { + controllerOld.DetachFromGeoView(geoView); + } + + if (e.NewValue is GeoViewController controllerNew) + { + controllerNew.AttachToGeoView(geoView); + } + } + else + { + throw new InvalidOperationException("This property must be attached to a GeoView."); + } + } +#endif + } +} From 363c4439f6c38bc0c0cb0ab71ce77b171dc42f5f Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Tue, 17 Oct 2023 11:19:43 -0700 Subject: [PATCH 2/7] Clean up and extensibility --- .../GeoViewControllerSampleVM.cs | 36 +++++++++- src/Toolkit/Toolkit/GeoViewController.cs | 71 +++++++++++-------- 2 files changed, 76 insertions(+), 31 deletions(-) diff --git a/src/Samples/Toolkit.SampleApp.WPF/Samples/GeoViewController/GeoViewControllerSampleVM.cs b/src/Samples/Toolkit.SampleApp.WPF/Samples/GeoViewController/GeoViewControllerSampleVM.cs index 0329ecb03..e6dabdb63 100644 --- a/src/Samples/Toolkit.SampleApp.WPF/Samples/GeoViewController/GeoViewControllerSampleVM.cs +++ b/src/Samples/Toolkit.SampleApp.WPF/Samples/GeoViewController/GeoViewControllerSampleVM.cs @@ -1,5 +1,8 @@ -using System; +#nullable enable +using System; +using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using System.Windows; using CommunityToolkit.Mvvm.Input; @@ -13,11 +16,11 @@ public partial class GeoViewControllerSampleVM { public Map Map { get; } = new Map(new Uri("https://www.arcgis.com/home/item.html?id=9f3a674e998f461580006e626611f9ad")); - public UI.GeoViewController Controller { get; } = new UI.GeoViewController(); + public IMyMapViewController Controller { get; } = new MyMapViewController(); [RelayCommand] public async Task OnGeoViewTapped(GeoViewInputEventArgs eventArgs) => await Identify(eventArgs.Position, eventArgs.Location); - + public async Task Identify(Point location, MapPoint? mapLocation) { Controller.DismissCallout(); @@ -25,10 +28,37 @@ public async Task Identify(Point location, MapPoint? mapLocation) if (result.FirstOrDefault()?.GeoElements?.FirstOrDefault() is GeoElement element) { Controller.ShowCalloutForGeoElement(element, location, new Esri.ArcGISRuntime.UI.CalloutDefinition(element)); + _ = Controller.PanToAsync(mapLocation); } else if (mapLocation is not null) { Controller.ShowCalloutAt(mapLocation, new Esri.ArcGISRuntime.UI.CalloutDefinition("No features found")); } } +} + +// Custom controller that extends the toolkit controller +public class MyMapViewController : UI.GeoViewController, IMyMapViewController +{ + public MapView? ConnectedMapView => ConnectedView as MapView; + + public Task PanToAsync(MapPoint? center) + { + if (center is null) + return Task.FromResult(false); + return ConnectedMapView?.SetViewpointCenterAsync(center) ?? Task.FromResult(false); + } + + public MapPoint? ScreenToLocation(Point screenLocation) => ConnectedMapView?.ScreenToLocation(screenLocation); +} + +// Custom interface for testability of VM +public interface IMyMapViewController +{ + void DismissCallout(); + void ShowCalloutForGeoElement(GeoElement element, Point tapPosition, ArcGISRuntime.UI.CalloutDefinition definition); + void ShowCalloutAt(MapPoint location, ArcGISRuntime.UI.CalloutDefinition definition); + Task> IdentifyLayersAsync(Point screenPoint, double tolerance, bool returnPopupsOnly = false, CancellationToken cancellationToken = default); + MapPoint? ScreenToLocation(Point screenLocation); + Task PanToAsync(MapPoint? center); } \ No newline at end of file diff --git a/src/Toolkit/Toolkit/GeoViewController.cs b/src/Toolkit/Toolkit/GeoViewController.cs index ef8ece674..f9de220cf 100644 --- a/src/Toolkit/Toolkit/GeoViewController.cs +++ b/src/Toolkit/Toolkit/GeoViewController.cs @@ -39,10 +39,25 @@ public class GeoViewController /// protected GeoView? ConnectedView => _geoViewWeak?.TryGetTarget(out var view) == true ? view : null; - private void AttachToGeoView(GeoView mv) + /// + /// Attaches a geoview to the controller, or detaches if null. + /// + /// The to attach, or null is detaching. + public void Attach(GeoView? geoView) + { + if (geoView is null) + { + if (ConnectedView is not null) + DetachFromGeoView(ConnectedView); + } + else + AttachToGeoView(geoView); + } + + private void AttachToGeoView(GeoView geoView) { var connectedView = ConnectedView; - if (connectedView == mv) + if (connectedView == geoView) { return; } @@ -53,35 +68,35 @@ private void AttachToGeoView(GeoView mv) connectedView = null; } // All event listeners must be weak to avoid holding on to the GeoView if the GeoViewController stays alive - _geoViewWeak = new WeakReference(mv); - _loadedListener = new WeakEventListener(this, mv) + _geoViewWeak = new WeakReference(geoView); + _loadedListener = new WeakEventListener(this, geoView) { OnEventAction = static (instance, source, eventArgs) => instance.GeoView_Loaded((GeoView)source!), OnDetachAction = static (instance, source, weakEventListener) => source.Loaded -= weakEventListener.OnEvent, }; - mv.Loaded += _loadedListener.OnEvent; + geoView.Loaded += _loadedListener.OnEvent; - _unloadedListener = new WeakEventListener(this, mv) + _unloadedListener = new WeakEventListener(this, geoView) { OnEventAction = static (instance, source, eventArgs) => instance.GeoView_Unloaded((GeoView)source!), OnDetachAction = static (instance, source, weakEventListener) => source.Unloaded -= weakEventListener.OnEvent, }; - mv.Loaded += _loadedListener.OnEvent; - mv.Unloaded += _unloadedListener.OnEvent; - OnGeoViewAttached(mv); - if (mv.IsLoaded) - GeoView_Loaded(mv); + geoView.Loaded += _loadedListener.OnEvent; + geoView.Unloaded += _unloadedListener.OnEvent; + OnGeoViewAttached(geoView); + if (geoView.IsLoaded) + GeoView_Loaded(geoView); } - private void DetachFromGeoView(GeoView mv) + private void DetachFromGeoView(GeoView geoView) { var connectedView = ConnectedView; - if (connectedView != null && connectedView == mv) + if (connectedView != null && connectedView == geoView) { _loadedListener?.Detach(); _unloadedListener?.Detach(); if (connectedView.IsLoaded) - GeoView_Unloaded(mv); + GeoView_Unloaded(geoView); OnGeoViewDetached(connectedView); _geoViewWeak = null; } @@ -119,10 +134,10 @@ protected virtual void OnGeoViewUnloaded(GeoView geoView) { } - private void GeoView_Loaded(GeoView sender) + private void GeoView_Loaded(GeoView geoView) { - Debug.Assert(sender == ConnectedView && ConnectedView is not null); - OnGeoViewLoaded(sender); + Debug.Assert(geoView == ConnectedView && ConnectedView is not null); + OnGeoViewLoaded(geoView); } private void GeoView_Unloaded(GeoView sender) @@ -134,38 +149,38 @@ private void GeoView_Unloaded(GeoView sender) #region Viewpoints /// - public Viewpoint? GetCurrentViewpoint(ViewpointType viewpointType) => ConnectedView?.GetCurrentViewpoint(viewpointType); + public virtual Viewpoint? GetCurrentViewpoint(ViewpointType viewpointType) => ConnectedView?.GetCurrentViewpoint(viewpointType); /// - public Task SetViewpointAsync(Viewpoint viewpoint) => ConnectedView?.SetViewpointAsync(viewpoint) ?? Task.FromResult(false); + public virtual Task SetViewpointAsync(Viewpoint viewpoint) => ConnectedView?.SetViewpointAsync(viewpoint) ?? Task.FromResult(false); /// - public Task SetViewpointAsync(Viewpoint viewpoint, TimeSpan duration) => ConnectedView?.SetViewpointAsync(viewpoint, duration) ?? Task.FromResult(false); + public virtual Task SetViewpointAsync(Viewpoint viewpoint, TimeSpan duration) => ConnectedView?.SetViewpointAsync(viewpoint, duration) ?? Task.FromResult(false); /// - public void SetViewpoint(Viewpoint viewpoint) => ConnectedView?.SetViewpoint(viewpoint); + public virtual void SetViewpoint(Viewpoint viewpoint) => ConnectedView?.SetViewpoint(viewpoint); #endregion Viewpoints #region Identify /// - public Task> IdentifyLayersAsync(Point screenPoint, double tolerance, bool returnPopupsOnly = false, CancellationToken cancellationToken = default) => + public virtual Task> IdentifyLayersAsync(Point screenPoint, double tolerance, bool returnPopupsOnly = false, CancellationToken cancellationToken = default) => ConnectedView?.IdentifyLayersAsync(screenPoint, tolerance, returnPopupsOnly, cancellationToken) ?? Task.FromResult>(new List().AsReadOnly()); /// - public Task IdentifyLayerAsync(Layer layer, Point screenPoint, double tolerance, bool returnPopupsOnly = false, CancellationToken cancellationToken = default) => + public virtual Task IdentifyLayerAsync(Layer layer, Point screenPoint, double tolerance, bool returnPopupsOnly = false, CancellationToken cancellationToken = default) => ConnectedView?.IdentifyLayerAsync(layer, screenPoint, tolerance, returnPopupsOnly, cancellationToken) ?? Task.FromResult(null!); /// - public Task> IdentifyGraphicsOverlaysAsync(Point screenPoint, double tolerance, bool returnPopupsOnly = false, long maximumResultsPerOverlay = 1) => + public virtual Task> IdentifyGraphicsOverlaysAsync(Point screenPoint, double tolerance, bool returnPopupsOnly = false, long maximumResultsPerOverlay = 1) => ConnectedView?.IdentifyGraphicsOverlaysAsync(screenPoint, tolerance, returnPopupsOnly, maximumResultsPerOverlay) ?? Task.FromResult>(new List().AsReadOnly()); /// - public Task IdentifyGraphicsOverlayAsync(GraphicsOverlay overlay, Point screenPoint, double tolerance, bool returnPopupsOnly = false, long maximumResults = 1) => + public virtual Task IdentifyGraphicsOverlayAsync(GraphicsOverlay overlay, Point screenPoint, double tolerance, bool returnPopupsOnly = false, long maximumResults = 1) => ConnectedView?.IdentifyGraphicsOverlayAsync(overlay, screenPoint, tolerance, returnPopupsOnly, maximumResults) ?? Task.FromResult(null!); #endregion Identify @@ -173,13 +188,13 @@ public Task IdentifyGraphicsOverlayAsync(Graphics #region Callouts /// - public void DismissCallout() => ConnectedView?.DismissCallout(); + public virtual void DismissCallout() => ConnectedView?.DismissCallout(); /// - public void ShowCalloutAt(Esri.ArcGISRuntime.Geometry.MapPoint location, CalloutDefinition definition) => ConnectedView?.ShowCalloutAt(location, definition); + public virtual void ShowCalloutAt(Esri.ArcGISRuntime.Geometry.MapPoint location, CalloutDefinition definition) => ConnectedView?.ShowCalloutAt(location, definition); /// - public void ShowCalloutForGeoElement(GeoElement element, Point tapPosition, CalloutDefinition definition) => ConnectedView?.ShowCalloutForGeoElement(element, tapPosition, definition); + public virtual void ShowCalloutForGeoElement(GeoElement element, Point tapPosition, CalloutDefinition definition) => ConnectedView?.ShowCalloutForGeoElement(element, tapPosition, definition); #endregion Callouts #if MAUI From 073d5168e3f3526b375883720674a41f68ec6e43 Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Wed, 1 Nov 2023 08:37:45 -0700 Subject: [PATCH 3/7] Update src/Toolkit/Toolkit/GeoViewController.cs Co-authored-by: Matvei Stefarov --- src/Toolkit/Toolkit/GeoViewController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Toolkit/Toolkit/GeoViewController.cs b/src/Toolkit/Toolkit/GeoViewController.cs index f9de220cf..184e8f360 100644 --- a/src/Toolkit/Toolkit/GeoViewController.cs +++ b/src/Toolkit/Toolkit/GeoViewController.cs @@ -167,7 +167,7 @@ private void GeoView_Unloaded(GeoView sender) /// public virtual Task> IdentifyLayersAsync(Point screenPoint, double tolerance, bool returnPopupsOnly = false, CancellationToken cancellationToken = default) => ConnectedView?.IdentifyLayersAsync(screenPoint, tolerance, returnPopupsOnly, cancellationToken) ?? - Task.FromResult>(new List().AsReadOnly()); + Task.FromResult>(Array.Empty()); /// public virtual Task IdentifyLayerAsync(Layer layer, Point screenPoint, double tolerance, bool returnPopupsOnly = false, CancellationToken cancellationToken = default) => From 837e99459d44928a4fb42fa873e87b9fcbbb8e66 Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Wed, 1 Nov 2023 08:37:59 -0700 Subject: [PATCH 4/7] Update src/Toolkit/Toolkit/GeoViewController.cs Co-authored-by: Matvei Stefarov --- src/Toolkit/Toolkit/GeoViewController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Toolkit/Toolkit/GeoViewController.cs b/src/Toolkit/Toolkit/GeoViewController.cs index 184e8f360..351f450c0 100644 --- a/src/Toolkit/Toolkit/GeoViewController.cs +++ b/src/Toolkit/Toolkit/GeoViewController.cs @@ -177,7 +177,7 @@ public virtual Task IdentifyLayerAsync(Layer layer, Point s /// public virtual Task> IdentifyGraphicsOverlaysAsync(Point screenPoint, double tolerance, bool returnPopupsOnly = false, long maximumResultsPerOverlay = 1) => ConnectedView?.IdentifyGraphicsOverlaysAsync(screenPoint, tolerance, returnPopupsOnly, maximumResultsPerOverlay) ?? - Task.FromResult>(new List().AsReadOnly()); + Task.FromResult>(Array.Empty()); /// public virtual Task IdentifyGraphicsOverlayAsync(GraphicsOverlay overlay, Point screenPoint, double tolerance, bool returnPopupsOnly = false, long maximumResults = 1) => From 622e7dd097c6db512c35ba6332c4a20a64124782 Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Wed, 1 Nov 2023 08:38:40 -0700 Subject: [PATCH 5/7] Update src/Toolkit/Toolkit/GeoViewController.cs Co-authored-by: Matvei Stefarov --- src/Toolkit/Toolkit/GeoViewController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Toolkit/Toolkit/GeoViewController.cs b/src/Toolkit/Toolkit/GeoViewController.cs index 351f450c0..324a8ff25 100644 --- a/src/Toolkit/Toolkit/GeoViewController.cs +++ b/src/Toolkit/Toolkit/GeoViewController.cs @@ -201,7 +201,7 @@ public virtual Task IdentifyGraphicsOverlayAsync( /// /// Identifies the attached property. /// - public static BindableProperty GeoViewControllerProperty = + public static readonly BindableProperty GeoViewControllerProperty = BindableProperty.CreateAttached( "GeoViewController", typeof(GeoViewController), From 9143693cad01f72125c0de4e90a321a4802dcaae Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Wed, 1 Nov 2023 15:04:32 -0700 Subject: [PATCH 6/7] Add null check --- src/Toolkit/Toolkit/GeoViewController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Toolkit/Toolkit/GeoViewController.cs b/src/Toolkit/Toolkit/GeoViewController.cs index 324a8ff25..4a21a534f 100644 --- a/src/Toolkit/Toolkit/GeoViewController.cs +++ b/src/Toolkit/Toolkit/GeoViewController.cs @@ -246,14 +246,14 @@ private static void OnGeoViewControllerChanged(BindableObject bindable, object o /// /// The from which to read the property value. /// The value of the XAML attached property on the target element. - public static GeoViewController? GetGeoViewController(GeoViewDPType geoView) => geoView.GetValue(GeoViewControllerProperty) as GeoViewController; + public static GeoViewController? GetGeoViewController(GeoViewDPType geoView) => geoView?.GetValue(GeoViewControllerProperty) as GeoViewController; /// /// Sets the value of the XAML attached property on the specified object. /// /// The target on which to set the XAML attached property. /// The property value to set. - public static void SetGeoViewController(GeoViewDPType geoView, GeoViewController? value) => geoView.SetValue(GeoViewControllerProperty, value); + public static void SetGeoViewController(GeoViewDPType geoView, GeoViewController? value) => geoView?.SetValue(GeoViewControllerProperty, value) ?? throw new System.ArgumentNullException(nameof(geoView)); private static void OnGeoViewControllerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { From d303e239c8163b112d4bc7f70dce40ae1d4b6caf Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Tue, 7 Nov 2023 14:09:17 -0800 Subject: [PATCH 7/7] Fix null annotations --- .../GeoViewControllerSampleVM.cs | 2 +- src/Toolkit/Toolkit/GeoViewController.cs | 21 ++++++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Samples/Toolkit.SampleApp.UWP/Samples/GeoViewController/GeoViewControllerSampleVM.cs b/src/Samples/Toolkit.SampleApp.UWP/Samples/GeoViewController/GeoViewControllerSampleVM.cs index 0fcc3bae9..c26499d8a 100644 --- a/src/Samples/Toolkit.SampleApp.UWP/Samples/GeoViewController/GeoViewControllerSampleVM.cs +++ b/src/Samples/Toolkit.SampleApp.UWP/Samples/GeoViewController/GeoViewControllerSampleVM.cs @@ -16,7 +16,7 @@ public class GeoViewControllerSampleVM public void OnGeoViewTapped(object sender, GeoViewInputEventArgs eventArgs) => Identify(eventArgs.Position, eventArgs.Location); - public async void Identify(Point location, MapPoint? mapLocation) + public async void Identify(Point location, MapPoint mapLocation) { Controller.DismissCallout(); var result = await Controller.IdentifyLayersAsync(location, 10); diff --git a/src/Toolkit/Toolkit/GeoViewController.cs b/src/Toolkit/Toolkit/GeoViewController.cs index 4a21a534f..063736968 100644 --- a/src/Toolkit/Toolkit/GeoViewController.cs +++ b/src/Toolkit/Toolkit/GeoViewController.cs @@ -168,11 +168,13 @@ private void GeoView_Unloaded(GeoView sender) public virtual Task> IdentifyLayersAsync(Point screenPoint, double tolerance, bool returnPopupsOnly = false, CancellationToken cancellationToken = default) => ConnectedView?.IdentifyLayersAsync(screenPoint, tolerance, returnPopupsOnly, cancellationToken) ?? Task.FromResult>(Array.Empty()); - + /// - public virtual Task IdentifyLayerAsync(Layer layer, Point screenPoint, double tolerance, bool returnPopupsOnly = false, CancellationToken cancellationToken = default) => - ConnectedView?.IdentifyLayerAsync(layer, screenPoint, tolerance, returnPopupsOnly, cancellationToken) ?? - Task.FromResult(null!); + public virtual async Task IdentifyLayerAsync(Layer layer, Point screenPoint, double tolerance, bool returnPopupsOnly = false, CancellationToken cancellationToken = default) + { + if (ConnectedView is null) return null; + return await ConnectedView.IdentifyLayerAsync(layer, screenPoint, tolerance, returnPopupsOnly, cancellationToken).ConfigureAwait(false); + } /// public virtual Task> IdentifyGraphicsOverlaysAsync(Point screenPoint, double tolerance, bool returnPopupsOnly = false, long maximumResultsPerOverlay = 1) => @@ -180,9 +182,12 @@ public virtual Task> IdentifyGraphi Task.FromResult>(Array.Empty()); /// - public virtual Task IdentifyGraphicsOverlayAsync(GraphicsOverlay overlay, Point screenPoint, double tolerance, bool returnPopupsOnly = false, long maximumResults = 1) => - ConnectedView?.IdentifyGraphicsOverlayAsync(overlay, screenPoint, tolerance, returnPopupsOnly, maximumResults) ?? - Task.FromResult(null!); + public virtual async Task IdentifyGraphicsOverlayAsync(GraphicsOverlay overlay, Point screenPoint, double tolerance, bool returnPopupsOnly = false, long maximumResults = 1) + { + if (ConnectedView is null) return null; + return await ConnectedView.IdentifyGraphicsOverlayAsync(overlay, screenPoint, tolerance, returnPopupsOnly, maximumResults).ConfigureAwait(false); + } + #endregion Identify #region Callouts @@ -253,7 +258,7 @@ private static void OnGeoViewControllerChanged(BindableObject bindable, object o /// /// The target on which to set the XAML attached property. /// The property value to set. - public static void SetGeoViewController(GeoViewDPType geoView, GeoViewController? value) => geoView?.SetValue(GeoViewControllerProperty, value) ?? throw new System.ArgumentNullException(nameof(geoView)); + public static void SetGeoViewController(GeoViewDPType geoView, GeoViewController? value) => geoView?.SetValue(GeoViewControllerProperty, value); private static void OnGeoViewControllerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {