using Xamarin.Forms; using TINK.View; using TINK.Model.Station; using System; using System.Linq; using TINK.Model.Bike; using TINK.Repository.Exception; using TINK.Model; using Serilog; using System.Collections.Generic; using System.Threading.Tasks; using System.ComponentModel; using Xamarin.Forms.GoogleMaps; using System.Collections.ObjectModel; #if USEFLYOUT using TINK.View.MasterDetail; #endif using TINK.Settings; using TINK.Model.Connector; using TINK.Model.Services.CopriApi; using TINK.Services.Permissions; using Xamarin.Essentials; using System.Threading; using TINK.MultilingualResources; using TINK.Services.BluetoothLock; using TINK.ViewModel.Info; using TINK.Repository; using TINK.Model.Services.Geolocation; #if !TRYNOTBACKSTYLE #endif namespace TINK.ViewModel.Map { public class MapPageViewModel : INotifyPropertyChanged { /// Holds the count of custom icons availalbe. private const int CUSTOM_ICONS_COUNT = 30; /// Reference on view service to show modal notifications and to perform navigation. private IViewService ViewService { get; } /// /// Holds the exception which occurred getting bikes occupied information. /// private Exception m_oException; /// /// Service to query/ manage permissions (location) of the app. /// private ILocationPermission PermissionsService { get; } /// /// Service to manage bluetooth stack. /// private Plugin.BLE.Abstractions.Contracts.IBluetoothLE BluetoothService { get; set; } /// Notifies view about changes. public event PropertyChangedEventHandler PropertyChanged; /// Object to manage update of view model objects from Copri. private IPollingUpdateTaskManager m_oViewUpdateManager; /// Holds whether to poll or not and the periode leght is polling is on. private PollingParameters Polling { get; set; } /// Reference on the tink app instance. private ITinkApp TinkApp { get; } /// Delegate to perform navigation. private INavigation m_oNavigation; #if USEFLYOUT /// Delegate to perform navigation. private INavigationMasterDetail m_oNavigationMasterDetail; #endif private ObservableCollection pins; public ObservableCollection Pins { get { if (pins == null) pins = new ObservableCollection(); // If view model is not binding context pins collection must be set programmatically. return pins; } set => pins = value; } /// Delegate to move map to region. private Action m_oMoveToRegionDelegate; /// False if user tabed on station marker to show bikes at a given station. private bool isMapPageEnabled = false; Model.Services.Geolocation.IGeolocation GeolocationService { get; } /// False if user tabed on station marker to show bikes at a given station. public bool IsMapPageEnabled { get => isMapPageEnabled; private set { if (isMapPageEnabled == value) return; isMapPageEnabled = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsMapPageEnabled))); } } /// Prevents an invalid instane to be created. /// Reference to tink app model. /// Delegate to center map and set zoom level. /// View service to notify user. /// Interface to navigate. public MapPageViewModel( ITinkApp tinkApp, ILocationPermission permissionsService, Plugin.BLE.Abstractions.Contracts.IBluetoothLE bluetoothService, IGeolocation geolocationService, Action moveToRegionDelegate, IViewService viewService, INavigation navigation) { TinkApp = tinkApp ?? throw new ArgumentException("Can not instantiate map page view model- object. No tink app object available."); PermissionsService = permissionsService ?? throw new ArgumentException($"Can not instantiate {nameof(MapPageViewModel)}. Permissions service object must never be null."); BluetoothService = bluetoothService ?? throw new ArgumentException($"Can not instantiate {nameof(MapPageViewModel)}. Bluetooth service object must never be null."); GeolocationService = geolocationService ?? throw new ArgumentException($"Can not instantiate {nameof(MapPageViewModel)}. Geolocation service object must never be null."); m_oMoveToRegionDelegate = moveToRegionDelegate ?? throw new ArgumentException("Can not instantiate map page view model- object. No move delegate available."); ViewService = viewService ?? throw new ArgumentException("Can not instantiate map page view model- object. No view available."); m_oNavigation = navigation ?? throw new ArgumentException("Can not instantiate map page view model- object. No navigation service available."); m_oViewUpdateManager = new IdlePollingUpdateTaskManager(); #if USEFLYOUT m_oNavigationMasterDetail = new EmptyNavigationMasterDetail(); #endif Polling = PollingParameters.NoPolling; tinkKonradToggleViewModel = new EmptyToggleViewModel(); IsConnected = TinkApp.GetIsConnected(); } /// Sets the stations filter to to apply (Konrad or TINK). public IGroupFilterMapPage ActiveFilterMap { get => tinkKonradToggleViewModel.FilterDictionary ?? new GroupFilterMapPage(); set { tinkKonradToggleViewModel = new TinkKonradToggleViewModel(value); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TinkColor))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(KonradColor))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsToggleVisible))); } } #if USEFLYOUT /// Delegate to perform navigation. public INavigationMasterDetail NavigationMasterDetail { set { m_oNavigationMasterDetail = value; } } #endif public Command PinClickedCommand => new Command( args => { OnStationClicked(args.Pin.Tag.ToString()); args.Handled = true; // Prevents map to be centered to selected pin. }); /// /// One time setup: Sets pins into map and connects to events. /// private void InitializePins(StationDictionary stations) { // Add pins to stations. Log.ForContext().Debug($"Request to draw {stations.Count} pins."); foreach (var station in stations) { if (station.Position == null) { // There should be no reason for a position object to be null but this alreay occurred in past. Log.ForContext().Error("Postion object of station {@l_oStation} is null.", station); continue; } var l_oPin = new Pin { Position = new Xamarin.Forms.GoogleMaps.Position(station.Position.Latitude, station.Position.Longitude), Label = long.TryParse(station.Id, out long stationId) && stationId > CUSTOM_ICONS_COUNT ? station.GetStationName() : string.Empty, // Stations with custom icons have already a id marker. No need for a label. Tag = station.Id, IsVisible = false, // Set to false to prevent showing default icons (flickering). }; Pins.Add(l_oPin); } } /// Update all stations from TINK. /// List of colors to apply. private void UpdatePinsColor(IList stationsColorList) { Log.ForContext().Debug($"Starting update of stations pins color for {stationsColorList.Count} stations..."); // Update colors of pins. for (int pinIndex = 0; pinIndex < stationsColorList.Count; pinIndex++) { var indexPartPrefix = int.TryParse(Pins[pinIndex].Tag.ToString(), out int stationId) && stationId <= CUSTOM_ICONS_COUNT ? $"{stationId}" // there is a station marker with index letter for given station id : "Open"; // there is no station marker. Use open marker. var colorPartPrefix = GetRessourceNameColorPart(stationsColorList[pinIndex]); var l_iName = $"{indexPartPrefix.ToString().PadLeft(2, '0')}_{colorPartPrefix}{(DeviceInfo.Platform == DevicePlatform.Android ? ".png" : string.Empty)}"; try { Pins[pinIndex].Icon = BitmapDescriptorFactory.FromBundle(l_iName); } catch (Exception l_oException) { Log.ForContext().Error("Station icon {l_strName} can not be loaded. {@l_oException}.", l_oException); Pins[pinIndex].Label = stationId.ToString(); Pins[pinIndex].Icon = BitmapDescriptorFactory.DefaultMarker(stationsColorList[pinIndex]); } Pins[pinIndex].IsVisible = true; } var pinsCount = Pins.Count; for (int pinIndex = stationsColorList.Count; pinIndex < pinsCount; pinIndex++) { Log.ForContext().Error($"Unexpected count of pins detected. Expected {stationsColorList.Count} but is {pinsCount}."); Pins[pinIndex].IsVisible = false; } Log.ForContext().Debug("Update of stations pins color done."); } /// Gets the color related part of the ressrouce name. /// Color to get name for. /// Resource name. private static string GetRessourceNameColorPart(Color color) { if (color == Color.Blue) { return "Blue"; } if (color == Color.Green) { return "Green"; } if (color == Color.LightBlue) { return "LightBlue"; } if (color == Color.Red) { return "Red"; } return color.ToString(); } /// /// Invoked when page is shown. /// Starts update process. /// /// Holds map page filter settings. /// Holds polling management object. /// If true whats new page will be shown. public async Task OnAppearing() { try { IsRunning = true; // Process map page. Polling = TinkApp.Polling; Log.ForContext().Information( $"{(Polling != null && Polling.IsActivated ? $"Map page is appearing. Update periode is {Polling.Periode.TotalSeconds} sec." : "Map page is appearing. Polling is off.")}" + $"Current UI language is {Thread.CurrentThread.CurrentUICulture.Name}."); // Update map page filter ActiveFilterMap = TinkApp.GroupFilterMapPage; ActionText = AppResources.ActivityTextRequestingLocationPermissions; Status status = await RequestLocationPermission(); ActionText = AppResources.ActivityTextMapLoadingStationsAndBikes; IsConnected = TinkApp.GetIsConnected(); Result resultStationsAndBikes = await TinkApp.GetConnector(IsConnected).Query.GetBikesAndStationsAsync(); TinkApp.Stations = resultStationsAndBikes.Response.StationsAll; await SetStationsOnMap(resultStationsAndBikes.Response.StationsAll); await HandleAuthCookieNotDefinedException(resultStationsAndBikes.Exception); // Update pin colors. Log.ForContext().Verbose("Starting update pins color..."); var colors = GetStationColors( Pins.Select(x => x.Tag.ToString()).ToList(), resultStationsAndBikes.Response.Bikes); // Update pins color form count of bikes located at station. UpdatePinsColor(colors); Log.ForContext().Verbose("Update pins color done."); // Move and scale before getting stations and bikes which takes some time. ActionText = AppResources.ActivityTextCenterMap; await MoveMapToCurrentPositionOfUser(status); m_oViewUpdateManager = CreateUpdateTask(); try { // Update bikes at station or my bikes depending on context. await m_oViewUpdateManager.StartUpdateAyncPeridically(Polling); } catch (Exception) { // Excpetions are handled insde update task; } Exception = resultStationsAndBikes.Exception; ActionText = ""; IsRunning = false; IsMapPageEnabled = true; } catch (Exception l_oException) { Log.ForContext().Error($"An error occurred switching view TINK/ Konrad.\r\n{l_oException.Message}"); IsRunning = false; await ViewService.DisplayAlert( "Fehler", $"Beim Anzeigen der Fahrradstandorte- Seite ist ein Fehler aufgetreten.\r\n{l_oException.Message}", "OK"); IsMapPageEnabled = true; } } /// /// Invoked when the auth cookie is not defined. /// private async Task HandleAuthCookieNotDefinedException(Exception exception) { if (exception?.GetType() == typeof(AuthcookieNotDefinedException)) { Log.ForContext().Error("Map page is shown (probable for the first time after startup of app) and COPRI auth cookie is not defined. {@l_oException}", exception); // COPRI reports an auth cookie error. await ViewService.DisplayAlert( AppResources.MessageWaring, AppResources.MessageMapPageErrorAuthcookieUndefined, AppResources.MessageAnswerOk); await TinkApp.GetConnector(IsConnected).Command.DoLogout(); TinkApp.ActiveUser.Logout(); } } /// /// Sets the available stations on the map. /// private async Task SetStationsOnMap(StationDictionary stations) { if (Pins.Count > 0 && Pins.Count != stations.Count) { // Either // - user logged in/ logged out which might lead to more/ less stations beeing available // - new stations were added/ existing ones remove Pins.Clear(); } // Check if there are alreay any pins to the map // i.e detecte first call of member OnAppearing after construction if (Pins.Count <= 0) { Log.ForContext().Debug($"{(ActiveFilterMap.GetGroup().Any() ? $"Active map filter is {string.Join(",", ActiveFilterMap.GetGroup())}." : "Map filter is off.")}"); // Map was not yet initialized. // Get stations from Copri Log.ForContext().Verbose("No pins detected on page."); if (stations.CopriVersion < CopriCallsStatic.UnsupportedVersionLower) { await ViewService.DisplayAlert( AppResources.MessageWaring, string.Format(AppResources.MessageCopriVersionIsOutdated, ContactPageViewModel.GetAppName(TinkApp.Uris.ActiveUri)), AppResources.MessageAnswerOk); Log.ForContext().Error($"Outdated version of app detected. Version expected is {stations.CopriVersion}."); } if (stations.CopriVersion >= CopriCallsStatic.UnsupportedVersionUpper) { await ViewService.DisplayAlert( AppResources.MessageWaring, string.Format(AppResources.MessageAppVersionIsOutdated, ContactPageViewModel.GetAppName(TinkApp.Uris.ActiveUri)), AppResources.MessageAnswerOk); Log.ForContext().Error($"Outdated version of app detected. Version expected is {stations.CopriVersion}."); } // Set pins to their positions on map. InitializePins(stations); Log.ForContext().Verbose("Update of pins done."); } } /// /// Moves the map to the current position of the user. /// If location permission hasn't been granted, the position is not adjusted. /// private async Task MoveMapToCurrentPositionOfUser(Status status) { if (status == Status.Granted) { ActionText = AppResources.ActivityTextCenterMap; if (TinkApp.CenterMapToCurrentLocation) { Location currentLocation = null; try { currentLocation = await GeolocationService.GetAsync(); } catch (Exception ex) { Log.ForContext().Error("Getting location failed. {Exception}", ex); } TinkApp.MapSpan = MapSpan.FromCenterAndRadius( new Xamarin.Forms.GoogleMaps.Position(currentLocation.Latitude, currentLocation.Longitude), TinkApp.MapSpan.Radius); TinkApp.Save(); } MoveAndScale(m_oMoveToRegionDelegate, TinkApp.MapSpan); } } /// /// Requests the location permission from the user. /// If the user declines, a dialog prompot is shown, telling the user to toggle the permission in the device settings. /// /// The permission status. private async Task RequestLocationPermission() { // Check location permission var status = await PermissionsService.CheckStatusAsync(); if (TinkApp.CenterMapToCurrentLocation && !GeolocationService.IsSimulation && status != Status.Granted) { status = await PermissionsService.RequestAsync(); if (status != Status.Granted) { var dialogResult = await ViewService.DisplayAlert( AppResources.MessageTitleHint, AppResources.MessageCenterMapLocationPermissionOpenDialog, AppResources.MessageAnswerYes, AppResources.MessageAnswerNo); if (dialogResult) { // User decided to give access to locations permissions. PermissionsService.OpenAppSettings(); ActionText = ""; IsRunning = false; IsMapPageEnabled = true; } } } return status; } /// Moves map and scales visible region depending on active filter. public static void MoveAndScale( Action moveToRegionDelegate, MapSpan currentMapSpan = null) { if (currentMapSpan != null) { // Move to current location. moveToRegionDelegate(currentMapSpan); return; } } /// Creates a update task object. /// Object to use for synchronization. private PollingUpdateTaskManager CreateUpdateTask() { // Start task which periodically updates pins. return new PollingUpdateTaskManager( () => GetType().Name, () => { try { Log.ForContext().Verbose("Entering update cycle."); Result resultStationsAndBikes; TinkApp.PostAction( unused => { ActionText = AppResources.ActivityTextUpdating; IsConnected = TinkApp.GetIsConnected(); }, null); resultStationsAndBikes = TinkApp.GetConnector(IsConnected).Query.GetBikesAndStationsAsync().Result; var exception = resultStationsAndBikes.Exception; if (exception != null) { Log.ForContext().Error("Getting bikes and stations in polling context failed with exception {Exception}.", exception); } // Check if there are alreay any pins to the map. // If no initialze pins. if (Pins.Count <= 0) { // Set pins to their positions on map. TinkApp.PostAction( unused => { InitializePins(resultStationsAndBikes.Response.StationsAll); }, null); } // Set/ update pins colors. var l_oColors = GetStationColors( Pins.Select(x => x.Tag.ToString()).ToList(), resultStationsAndBikes.Response.Bikes); // Update pins color form count of bikes located at station. TinkApp.PostAction( unused => { UpdatePinsColor(l_oColors); ActionText = string.Empty; Exception = resultStationsAndBikes.Exception; }, null); Log.ForContext().Verbose("Leaving update cycle."); } catch (Exception exception) { Log.ForContext().Error("Getting stations and bikes from update task failed. {Exception}", exception); TinkApp.PostAction( unused => { Exception = exception; ActionText = string.Empty; }, null); Log.ForContext().Verbose("Leaving update cycle."); return; } }); } /// /// Invoked when pages is closed/ hidden. /// Stops update process. /// public async Task OnDisappearing() { Log.Information("Map page is disappearing..."); await m_oViewUpdateManager.StopUpdatePeridically(); } /// User clicked on a bike. /// Id of station user clicked on. public async void OnStationClicked(string selectedStationId) { try { Log.ForContext().Information($"User taped station {selectedStationId}."); // Lock action to prevent multiple instances of "BikeAtStation" being opened. IsMapPageEnabled = false; TinkApp.SelectedStation = TinkApp.Stations.FirstOrDefault(x => x.Id == selectedStationId) ?? new Station(selectedStationId, new List(), null); // Station might not be in list StationDictinaly because this list is not updatd in background task. #if TRYNOTBACKSTYLE m_oNavigation.ShowPage( typeof(BikesAtStationPage), p_strStationName); #else // Show page. await ViewService.PushAsync(ViewTypes.BikesAtStation); IsMapPageEnabled = true; ActionText = ""; } catch (Exception exception) { IsMapPageEnabled = true; ActionText = ""; Log.ForContext().Error("Fehler beim Öffnen der Ansicht \"Fahrräder an Station\" aufgetreten. {Exception}", exception); await ViewService.DisplayAlert( "Fehler", $"Fehler beim Öffnen der Ansicht \"Fahrräder an Station\" aufgetreten. {exception.Message}", "OK"); } #endif } /// /// Gets the list of station color for all stations. /// /// Station id list to get color for. /// private static IList GetStationColors( IEnumerable stationsId, BikeCollection bikesAll) { if (stationsId == null) { Log.ForContext().Debug("No stations available to update color for."); return new List(); } if (bikesAll == null) { // If object is null an error occurred querrying bikes availalbe or bikes occpied which results in an unknown state. Log.ForContext().Error("No bikes available to determine pins color."); return new List(stationsId.Select(x => Color.Blue)); } // Get state for each station. var colors = new List(); foreach (var stationId in stationsId) { // Get color of given station. var bikesAtStation = bikesAll.Where(x => x.CurrentStation == stationId).ToList(); if (bikesAtStation.FirstOrDefault(x => x.State.Value != Model.State.InUseStateEnum.Disposable) != null) { // There is at least one requested or booked bike colors.Add(Color.LightBlue); continue; } if (bikesAtStation.ToList().Count > 0) { // There is at least one bike available colors.Add(Color.Green); continue; } colors.Add(Color.Red); } return colors; } /// /// Exception which occurred getting bike information. /// public Exception Exception { get { return m_oException; } private set { var statusInfoText = StatusInfoText; m_oException = value; if (statusInfoText == StatusInfoText) { // Nothing to do because value did not change. return; } PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StatusInfoText))); } } /// Holds info about current action. private string actionText; /// Holds info about current action. private string ActionText { get => actionText; set { var statusInfoText = StatusInfoText; actionText = value; if (statusInfoText == StatusInfoText) { // Nothing to do because value did not change. return; } PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StatusInfoText))); } } /// Used to block more than on copri requests at a given time. private bool isRunning = false; /// /// True if any action can be performed (request and cancel request) /// public bool IsRunning { get => isRunning; set { if (value == isRunning) return; Log.ForContext().Debug($"Switch value of {nameof(isRunning)} to {value}."); isRunning = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsRunning))); } } /// Holds information whether app is connected to web or not. private bool? isConnected = null; /// Exposes the is connected state. private bool IsConnected { get => isConnected ?? false; set { var statusInfoText = StatusInfoText; isConnected = value; if (statusInfoText == StatusInfoText) { // Nothing to do. return; } PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StatusInfoText))); } } /// Holds the status information text. public string StatusInfoText { get { if (Exception != null) { // An error occurred getting data from copri. return TinkApp.IsReportLevelVerbose ? Exception.GetShortErrorInfoText() : AppResources.ActivityTextException; } if (!IsConnected) { return AppResources.ActivityTextConnectionStateOffline; } return ActionText ?? string.Empty; } } /// Command object to bind login button to view model. public System.Windows.Input.ICommand OnToggleTinkToKonrad => new Xamarin.Forms.Command(async () => await ToggleTinkToKonrad()); /// Command object to bind login button to view model. public System.Windows.Input.ICommand OnToggleKonradToTink => new Xamarin.Forms.Command(async () => await ToggleKonradToTink()); /// Manages toggle functionality. private ITinkKonradToggleViewModel tinkKonradToggleViewModel; /// User request to toggle from TINK to Konrad. public async Task ToggleTinkToKonrad() { if (tinkKonradToggleViewModel.CurrentFilter == FilterHelper.FILTERKONRAD) { // Konrad is already activated, nothing to do. return; } Log.ForContext().Information("User toggles to Konrad."); await ActivateFilter(FilterHelper.FILTERTINKGENERAL); } /// User request to toggle from TINK to Konrad. public async Task ToggleKonradToTink() { if (tinkKonradToggleViewModel.CurrentFilter == FilterHelper.FILTERTINKGENERAL) { // Konrad is already activated, nothing to do. return; } Log.ForContext().Information("User toggles to TINK."); await ActivateFilter(FilterHelper.FILTERKONRAD); } /// User request to toggle from TINK to Konrad. private async Task ActivateFilter(string p_strSelectedFilter) { try { Log.ForContext().Information($"Request to toggle to \"{p_strSelectedFilter}\"."); // Stop polling. await m_oViewUpdateManager.StopUpdatePeridically(); // Clear error info. Exception = null; // Toggle view tinkKonradToggleViewModel = new TinkKonradToggleViewModel(ActiveFilterMap).DoToggle(); ActiveFilterMap = tinkKonradToggleViewModel.FilterDictionary; TinkApp.GroupFilterMapPage = ActiveFilterMap; TinkApp.Save(); TinkApp.UpdateConnector(); Pins.Clear(); // Check location permission var status = await PermissionsService.CheckStatusAsync(); if (TinkApp.CenterMapToCurrentLocation && !GeolocationService.IsSimulation && status != Status.Granted) { var permissionResult = await PermissionsService.RequestAsync(); if (permissionResult != Status.Granted) { var dialogResult = await ViewService.DisplayAlert( AppResources.MessageTitleHint, AppResources.MessageBikesManagementLocationPermission, "Ja", "Nein"); if (dialogResult) { // User decided to give access to locations permissions. PermissionsService.OpenAppSettings(); IsMapPageEnabled = true; ActionText = ""; return; } } // Do not use property .State to get bluetooth state due // to issue https://hausource.visualstudio.com/TINK/_workitems/edit/116 / // see https://github.com/xabre/xamarin-bluetooth-le/issues/112#issuecomment-380994887 if (await BluetoothService.GetBluetoothState() != Plugin.BLE.Abstractions.Contracts.BluetoothState.On) { await ViewService.DisplayAlert( AppResources.MessageTitleHint, AppResources.MessageBikesManagementBluetoothActivation, AppResources.MessageAnswerOk); IsMapPageEnabled = true; ActionText = ""; return; } } // Move and scale before getting stations and bikes which takes some time. if (TinkApp.CenterMapToCurrentLocation) { Location currentLocation = null; try { currentLocation = await GeolocationService.GetAsync(); } catch (Exception ex) { Log.ForContext().Error("Getting location failed. {Exception}", ex); } TinkApp.MapSpan = MapSpan.FromCenterAndRadius( new Xamarin.Forms.GoogleMaps.Position(currentLocation.Latitude, currentLocation.Longitude), TinkApp.MapSpan.Radius); TinkApp.Save(); } // Update stations MoveAndScale(m_oMoveToRegionDelegate, TinkApp.MapSpan); IsConnected = TinkApp.GetIsConnected(); var resultStationsAndBikes = await TinkApp.GetConnector(IsConnected).Query.GetBikesAndStationsAsync(); // Set pins to their positions on map. InitializePins(resultStationsAndBikes.Response.StationsAll); Log.ForContext().Verbose("Update of pins on toggle done..."); // Update pin colors. Log.ForContext().Verbose("Starting update pins color on toggle..."); var l_oColors = GetStationColors( Pins.Select(x => x.Tag.ToString()).ToList(), resultStationsAndBikes.Response.Bikes); // Update pins color form count of bikes located at station. UpdatePinsColor(l_oColors); Log.ForContext().Verbose("Update pins color done."); try { // Update bikes at station or my bikes depending on context. await m_oViewUpdateManager.StartUpdateAyncPeridically(Polling); } catch (Exception) { // Excpetions are handled insde update task; } Log.ForContext().Information($"Toggle to \"{p_strSelectedFilter}\" done."); } catch (Exception l_oException) { Log.ForContext().Error("An error occurred switching view TINK/ Konrad.{}"); await ViewService.DisplayAlert( "Fehler", $"Beim Umschalten TINK/ Konrad ist ein Fehler aufgetreten.\r\n{l_oException.Message}", "OK"); } } public Color TinkColor => tinkKonradToggleViewModel.TinkColor; public Color KonradColor => tinkKonradToggleViewModel.KonradColor; public bool IsToggleVisible => tinkKonradToggleViewModel.IsToggleVisible; } }