using Xamarin.Forms; using TINK.View; using TINK.Model.Station; using System; using System.Linq; using TINK.Model.Bike; using TINK.Model.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; using TINK.View.MasterDetail; using TINK.Settings; using TINK.Model.Connector; using TINK.Model.Services.CopriApi; using Plugin.Permissions; using Xamarin.Essentials; using Plugin.BLE; using System.Threading; using TINK.MultilingualResources; using TINK.Services.BluetoothLock; using TINK.Model.Services.CopriApi.ServerUris; using TINK.ViewModel.Info; #if !TRYNOTBACKSTYLE #endif namespace TINK.ViewModel.Map { public class MapPageViewModel : INotifyPropertyChanged { /// Holds the count of custom idcons availalbe. private const int CUSTOM_ICONS_COUNT = 30; /// Reference on view servcie to show modal notifications and to perform navigation. private IViewService m_oViewService; /// /// Holds the exception which occurred getting bikes occupied information. /// private Exception m_oException; /// 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; /// Delegate to perform navigation. private INavigationMasterDetail m_oNavigationMasterDetail; 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; /// 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, Action p_oMoveToRegionDelegate, IViewService p_oViewService, INavigation p_oNavigation) { TinkApp = tinkApp ?? throw new ArgumentException("Can not instantiate map page view model- object. No tink app object available."); m_oMoveToRegionDelegate = p_oMoveToRegionDelegate ?? throw new ArgumentException("Can not instantiate map page view model- object. No move delegate available."); m_oViewService = p_oViewService ?? throw new ArgumentException("Can not instantiate map page view model- object. No view available."); m_oNavigation = p_oNavigation ?? throw new ArgumentException("Can not instantiate map page view model- object. No navigation service available."); m_oViewUpdateManager = new IdlePollingUpdateTaskManager(); m_oNavigationMasterDetail = new EmptyNavigationMasterDetail(); 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))); } } /// Delegate to perform navigation. public INavigationMasterDetail NavigationMasterDetail { set { m_oNavigationMasterDetail = value; } } public Command PinClickedCommand => new Command( args => { OnStationClicked(int.Parse(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 p_oStations) { // Add pins to stations. Log.ForContext().Debug($"Request to draw {p_oStations.Count} pins."); foreach (var l_oStation in p_oStations) { if (l_oStation.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.", l_oStation); continue; } var l_oPin = new Pin { Position = new Xamarin.Forms.GoogleMaps.Position(l_oStation.Position.Latitude, l_oStation.Position.Longitude), Label = l_oStation.Id > CUSTOM_ICONS_COUNT ? l_oStation.GetStationName() : string.Empty, // Stations with custom icons have already a id marker. No need for a label. Tag = l_oStation.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 p_oStationsColorList) { Log.ForContext().Debug($"Starting update of stations pins color for {p_oStationsColorList.Count} stations..."); // Update colors of pins. for (int l_iPinIndex = 0; l_iPinIndex < p_oStationsColorList.Count; l_iPinIndex++) { var l_iStationId = int.Parse(Pins[l_iPinIndex].Tag.ToString()); var indexPartPrefix = l_iStationId <= CUSTOM_ICONS_COUNT ? $"{l_iStationId}" // there is a station marker with index letter for given station id : "Open"; // there is no station marker. Use open marker. var colorPartPrefix = GetRessourceNameColorPart(p_oStationsColorList[l_iPinIndex]); var l_iName = $"{indexPartPrefix.ToString().PadLeft(2, '0')}_{colorPartPrefix}{(DeviceInfo.Platform == DevicePlatform.Android ? ".png" : string.Empty)}"; try { Pins[l_iPinIndex].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[l_iPinIndex].Label = l_iStationId.ToString(); Pins[l_iPinIndex].Icon = BitmapDescriptorFactory.DefaultMarker(p_oStationsColorList[l_iPinIndex]); } Pins[l_iPinIndex].IsVisible = true; } var pinsCount = Pins.Count; for (int pinIndex = p_oStationsColorList.Count; pinIndex < pinsCount; pinIndex++) { Log.ForContext().Error($"Unexpected count of pins detected. Expected {p_oStationsColorList.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 p_oColor) { if (p_oColor == Color.Blue) { return "Blue"; } if (p_oColor == Color.Green) { return "Green"; } if (p_oColor == Color.LightBlue) { return "LightBlue"; } if (p_oColor == Color.Red) { return "Red"; } return p_oColor.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; if (Pins.Count <= 0) { ActionText = AppResources.ActivityTextMyBikesLoadingBikes; // Check location permission var _permissions = TinkApp.Permissions; var status = await _permissions.CheckPermissionStatusAsync(); if (TinkApp.CenterMapToCurrentLocation && !TinkApp.GeolocationServices.Active.IsSimulation && status != Plugin.Permissions.Abstractions.PermissionStatus.Granted) { var permissionResult = await _permissions.RequestPermissionAsync(); if (permissionResult != Plugin.Permissions.Abstractions.PermissionStatus.Granted) { var dialogResult = await m_oViewService.DisplayAlert( AppResources.MessageTitleHint, AppResources.MessageCenterMapLocationPermissionOpenDialog, AppResources.MessageAnswerYes, AppResources.MessageAnswerNo); if (dialogResult) { // User decided to give access to locations permissions. _permissions.OpenAppSettings(); ActionText = ""; IsRunning = false; IsMapPageEnabled = true; return; } } } // Move and scale before getting stations and bikes which takes some time. ActionText = AppResources.ActivityTextCenterMap; Location currentLocation = null; try { currentLocation = TinkApp.CenterMapToCurrentLocation ? await TinkApp.GeolocationServices.Active.GetAsync() : null; } catch (Exception ex) { Log.ForContext().Error("Getting location failed. {Exception}", ex); } MoveAndScale(m_oMoveToRegionDelegate, TinkApp.Uris.ActiveUri, ActiveFilterMap, currentLocation); } ActionText = AppResources.ActivityTextMapLoadingStationsAndBikes; IsConnected = TinkApp.GetIsConnected(); var resultStationsAndBikes = await TinkApp.GetConnector(IsConnected).Query.GetBikesAndStationsAsync(); // 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 (resultStationsAndBikes.Response.StationsAll.CopriVersion >= new Version(4, 1)) { await m_oViewService.DisplayAlert( "Warnung", string.Format(AppResources.MessageAppVersionIsOutdated, ContactPageViewModel.GetAppName(TinkApp.Uris.ActiveUri)), "OK"); Log.ForContext().Error($"Outdated version of app detected. Version expected is {resultStationsAndBikes.Response.StationsAll.CopriVersion}."); } // Set pins to their positions on map. InitializePins(resultStationsAndBikes.Response.StationsAll); Log.ForContext().Verbose("Update of pins done."); } if (resultStationsAndBikes.Exception?.GetType() == typeof(AuthcookieNotDefinedException)) { Log.ForContext().Error("Map page is shown (probable for the first time after startup of app) and COPRI copri an auth cookie not defined error.{@l_oException}", resultStationsAndBikes.Exception); // COPRI reports an auth cookie error. await m_oViewService.DisplayAlert( AppResources.MessageWaring, AppResources.MessageMapPageErrorAuthcookieUndefined, AppResources.MessageAnswerOk); await TinkApp.GetConnector(IsConnected).Command.DoLogout(); TinkApp.ActiveUser.Logout(); } // Update pin colors. Log.ForContext().Verbose("Starting update pins color..."); 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); m_oViewUpdateManager = CreateUpdateTask(); 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; } 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 m_oViewService.DisplayAlert( "Fehler", $"Beim Anzeigen der Fahrradstandorte- Seite ist ein Fehler aufgetreten.\r\n{l_oException.Message}", "OK"); IsMapPageEnabled = true; } } /// Moves map and scales visible region depending on active filter. public static void MoveAndScale( Action moveToRegionDelegate, Uri activeUri, IGroupFilterMapPage groupFilterMapPage, Location currentLocation = null) { if (currentLocation != null) { // Move to current location. moveToRegionDelegate(MapSpan.FromCenterAndRadius( new Xamarin.Forms.GoogleMaps.Position(currentLocation.Latitude, currentLocation.Longitude), Distance.FromKilometers(1.0))); return; } if (activeUri.AbsoluteUri == CopriServerUriList.SHAREE_LIVE || activeUri.AbsoluteUri == CopriServerUriList.SHAREE_DEVEL) { // Center map to Freiburg moveToRegionDelegate(MapSpan.FromCenterAndRadius( new Xamarin.Forms.GoogleMaps.Position(47.995865, 7.815086), Distance.FromKilometers(2.9))); return; } // Depending on whether TINK or Conrad is active set center of map and scale. if (groupFilterMapPage.GetGroup().Contains(FilterHelper.FILTERKONRAD)) { // Konrad is activated, moveToRegionDelegate(MapSpan.FromCenterAndRadius( new Xamarin.Forms.GoogleMaps.Position(47.680, 9.180), Distance.FromKilometers(2.9))); } else { // TINK moveToRegionDelegate(MapSpan.FromCenterAndRadius( new Xamarin.Forms.GoogleMaps.Position(47.667, 9.172), Distance.FromKilometers(0.9))); } } /// 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 = "Aktualisiere..."; 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(int selectedStationId) { try { Log.ForContext().Information($"User taped station {selectedStationId}."); // Lock action to prevent multiple instances of "BikeAtStation" being opened. IsMapPageEnabled = false; TinkApp.SelectedStation = selectedStationId; #if TRYNOTBACKSTYLE m_oNavigation.ShowPage( typeof(BikesAtStationPage), p_strStationName); #else // Show page. await m_oViewService.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 m_oViewService.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 p_oStationsId, BikeCollection bikesAll) { if (p_oStationsId == 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(p_oStationsId.Select(x => Color.Blue)); } // Get state for each station. var l_oColors = new List(); foreach (var l_strStationId in p_oStationsId) { if (int.TryParse(l_strStationId, out int l_iStationId) == false) { // Station id is not valid. Log.ForContext().Error($"A station id {l_strStationId} is invalid (not integer)."); l_oColors.Add(Color.Blue); continue; } // Get color of given station. var l_oBikesAtStation = bikesAll.Where(x => x.CurrentStation == l_iStationId); if (l_oBikesAtStation.FirstOrDefault(x => x.State.Value != Model.State.InUseStateEnum.Disposable) != null) { // There is at least one requested or booked bike l_oColors.Add(Color.LightBlue); continue; } if (l_oBikesAtStation.ToList().Count > 0) { // There is at least one bike available l_oColors.Add(Color.Green); continue; } l_oColors.Add(Color.Red); } return l_oColors; } /// /// 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 Exception.GetShortErrorInfoText(); } if (!IsConnected) { return "Offline."; } 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 _permissions = TinkApp.Permissions; var status = await _permissions.CheckPermissionStatusAsync(); if (TinkApp.CenterMapToCurrentLocation && !TinkApp.GeolocationServices.Active.IsSimulation && status != Plugin.Permissions.Abstractions.PermissionStatus.Granted) { var permissionResult = await _permissions.RequestPermissionAsync(); if (permissionResult != Plugin.Permissions.Abstractions.PermissionStatus.Granted) { var dialogResult = await m_oViewService.DisplayAlert( AppResources.MessageTitleHint, AppResources.MessageBikesManagementLocationPermission, "Ja", "Nein"); if (dialogResult) { // User decided to give access to locations permissions. _permissions.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 CrossBluetoothLE.Current.GetBluetoothState() != Plugin.BLE.Abstractions.Contracts.BluetoothState.On) { await m_oViewService.DisplayAlert( AppResources.MessageTitleHint, AppResources.MessageBikesManagementBluetoothActivation, AppResources.MessageAnswerOk); IsMapPageEnabled = true; ActionText = ""; return; } } // Move and scale before getting stations and bikes which takes some time. Location currentLocation = null; try { currentLocation = TinkApp.CenterMapToCurrentLocation ? await TinkApp.GeolocationServices.Active.GetAsync() : null; } catch (Exception ex) { Log.ForContext().Error("Getting location failed. {Exception}", ex); } // Update stations // Depending on whether TINK or Conrad is active set center of map and scale. MoveAndScale(m_oMoveToRegionDelegate, TinkApp.Uris.ActiveUri, ActiveFilterMap, currentLocation); 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 m_oViewService.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; } }