From fb1756b82bf1e0aa5e774491014501ee2f25ac39 Mon Sep 17 00:00:00 2001 From: Max Mannstein Date: Sun, 24 Aug 2025 19:27:44 +0200 Subject: [PATCH] Implement change detection in ProfileViewModel and ProfileWalkerViewModel; add original state tracking and update UI to reflect changes with a floating save button in ProfileView and ProfileWalkerView. --- .../ViewModels/ProfileViewModel.cs | 201 +++++++++++++ .../ViewModels/ProfileWalkerViewModel.cs | 279 ++++++++++++++++++ .../gehGassiApp/Views/ProfileView.xaml | 67 +++-- .../gehGassiApp/Views/ProfileWalkerView.xaml | 70 +++-- 4 files changed, 564 insertions(+), 53 deletions(-) diff --git a/gehGassiApp/gehGassiApp/ViewModels/ProfileViewModel.cs b/gehGassiApp/gehGassiApp/ViewModels/ProfileViewModel.cs index b2cb984..97c36f0 100644 --- a/gehGassiApp/gehGassiApp/ViewModels/ProfileViewModel.cs +++ b/gehGassiApp/gehGassiApp/ViewModels/ProfileViewModel.cs @@ -159,6 +159,39 @@ namespace gehGassiApp.ViewModels [ObservableProperty] private Country _selectedMainResidence; + /// + /// Gibt an ob sich Daten geändert haben + /// + [ObservableProperty] + private bool _hasChanges; + + /// + /// Original AppUser für Change Detection + /// + private AppUser _originalAppUser; + + /// + /// State Class für Change Detection + /// + private class OriginalProfileState + { + public string FirstName { get; set; } = ""; + public string LastName { get; set; } = ""; + public string Photo { get; set; } = ""; + public DateTime? BirthDate { get; set; } + public string AddressLine1 { get; set; } = ""; + public string AddressLine2 { get; set; } = ""; + public string Zip { get; set; } = ""; + public string City { get; set; } = ""; + public string Mobile { get; set; } = ""; + public string Phone { get; set; } = ""; + public string CountryIso2 { get; set; } = ""; + public string StateCode { get; set; } = ""; + public Sex Sex { get; set; } + public string NationalityIso2 { get; set; } = ""; + public string MainResidenceIso2 { get; set; } = ""; + } private OriginalProfileState _originalState; + #region Extended partial void OnSelectedCountryChanged(Country value) @@ -173,12 +206,16 @@ namespace gehGassiApp.ViewModels var states = _countryService.GetStatesAsync(value.Iso2).Result; States = states.ToObservableCollection(); } + + CheckForChanges(); } partial void OnSelectedStateChanged(State value) { if (value != null) AddressSelectionValid = true; + + CheckForChanges(); } partial void OnBirthDateChanged(DateTime? value) @@ -187,6 +224,58 @@ namespace gehGassiApp.ViewModels return; if (value.HasValue) BirthDay = value.Value.ToString("d", CultureInfo.CurrentCulture); + + CheckForChanges(); + } + + partial void OnSelectedSexChanged(SexItem value) + { + CheckForChanges(); + } + + partial void OnSelectedNationalityChanged(Country value) + { + CheckForChanges(); + } + + partial void OnSelectedMainResidenceChanged(Country value) + { + CheckForChanges(); + } + + partial void OnAppUserChanged(AppUser value) + { + // Entferne alte Event-Handler falls vorhanden + if (_appUser != null) + { + _appUser.PropertyChanged -= OnAppUserPropertyChanged; + if (_appUser.Address != null) + _appUser.Address.PropertyChanged -= OnAppUserPropertyChanged; + if (_appUser.Contact != null) + _appUser.Contact.PropertyChanged -= OnAppUserPropertyChanged; + } + + // Registriere neue Event-Handler + if (value != null) + { + value.PropertyChanged += OnAppUserPropertyChanged; + if (value.Address != null) + value.Address.PropertyChanged += OnAppUserPropertyChanged; + if (value.Contact != null) + value.Contact.PropertyChanged += OnAppUserPropertyChanged; + } + + CheckForChanges(); + } + + partial void OnIsNewPhotoChanged(bool value) + { + CheckForChanges(); + } + + partial void OnPhotoPathChanged(string value) + { + CheckForChanges(); } #endregion @@ -545,11 +634,118 @@ namespace gehGassiApp.ViewModels _countryHandlerEnabled = true; + // Speichere ursprüngliche Werte für Change Detection + SaveOriginalValues(); + IsLoading = false; } + #region Event Handlers for Change Detection + + /// + /// Wird aufgerufen wenn sich eine Property des AppUser oder seiner Unterobjekte ändert + /// + private void OnAppUserPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + CheckForChanges(); + } + + #endregion + #region Helper + /// + /// Prüft ob sich Daten geändert haben + /// + private void CheckForChanges() + { + if (IsLoading || _originalState == null || AppUser == null) + return; + + try + { + // Prüfe AppUser Properties + var hasChanges = + AppUser.FirstName != _originalState.FirstName || + AppUser.LastName != _originalState.LastName || + AppUser.Photo != _originalState.Photo || + BirthDate != _originalState.BirthDate; + + // Prüfe Address Properties + if (AppUser.Address != null) + { + hasChanges = hasChanges || + (AppUser.Address.AddressLine1 ?? "") != _originalState.AddressLine1 || + (AppUser.Address.AddressLine2 ?? "") != _originalState.AddressLine2 || + (AppUser.Address.Zip ?? "") != _originalState.Zip || + (AppUser.Address.City ?? "") != _originalState.City; + } + + // Prüfe Contact Properties + if (AppUser.Contact != null) + { + hasChanges = hasChanges || + (AppUser.Contact.Mobile ?? "") != _originalState.Mobile || + (AppUser.Contact.Phone ?? "") != _originalState.Phone; + } + + // Prüfe Selections + if (SelectedCountry?.Iso2 != _originalState.CountryIso2) + hasChanges = true; + if (SelectedState?.Code != _originalState.StateCode) + hasChanges = true; + if (SelectedSex?.Value != _originalState.Sex) + hasChanges = true; + if (SelectedNationality?.Iso2 != _originalState.NationalityIso2) + hasChanges = true; + if (SelectedMainResidence?.Iso2 != _originalState.MainResidenceIso2) + hasChanges = true; + + // Prüfe ob neues Foto vorhanden + if (IsNewPhoto && !string.IsNullOrWhiteSpace(PhotoPath)) + hasChanges = true; + + HasChanges = hasChanges; + } + catch (Exception) + { + // Fallback: Bei Fehlern Changes auf true setzen + HasChanges = true; + } + } + + /// + /// Speichert die ursprünglichen Werte für Change Detection + /// + private void SaveOriginalValues() + { + try + { + _originalState = new OriginalProfileState + { + FirstName = AppUser?.FirstName ?? "", + LastName = AppUser?.LastName ?? "", + Photo = AppUser?.Photo ?? "", + BirthDate = BirthDate, + AddressLine1 = AppUser?.Address?.AddressLine1 ?? "", + AddressLine2 = AppUser?.Address?.AddressLine2 ?? "", + Zip = AppUser?.Address?.Zip ?? "", + City = AppUser?.Address?.City ?? "", + Mobile = AppUser?.Contact?.Mobile ?? "", + Phone = AppUser?.Contact?.Phone ?? "", + CountryIso2 = SelectedCountry?.Iso2 ?? "", + StateCode = SelectedState?.Code ?? "", + Sex = SelectedSex?.Value ?? Sex.Undefined, + NationalityIso2 = SelectedNationality?.Iso2 ?? "", + MainResidenceIso2 = SelectedMainResidence?.Iso2 ?? "" + }; + } + catch (Exception) + { + _originalState = new OriginalProfileState(); + } + } + /// /// Laden eines Fotos /// @@ -638,6 +834,11 @@ namespace gehGassiApp.ViewModels IsNewPhoto = false; await Task.Delay(Core.Common.Constants.AnimationDelayDetails); await LoadAsync(); + + // Speichere Original-Werte für Change Detection + SaveOriginalValues(); + HasChanges = false; + IsBusy = false; } diff --git a/gehGassiApp/gehGassiApp/ViewModels/ProfileWalkerViewModel.cs b/gehGassiApp/gehGassiApp/ViewModels/ProfileWalkerViewModel.cs index 0f97463..2cb36d9 100644 --- a/gehGassiApp/gehGassiApp/ViewModels/ProfileWalkerViewModel.cs +++ b/gehGassiApp/gehGassiApp/ViewModels/ProfileWalkerViewModel.cs @@ -172,6 +172,60 @@ namespace gehGassiApp.ViewModels [ObservableProperty] private Country _selectedMainResidence; + /// + /// Gibt an ob sich Daten geändert haben + /// + [ObservableProperty] + private bool _hasChanges; + + /// + /// Original AppUser für Change Detection + /// + private AppUser _originalAppUser; + + /// + /// Original WalkerProfile für Change Detection + /// + private DogWalkerProfile _originalWalkerProfile; + + /// + /// Original State für Change Detection + /// + private class OriginalWalkerProfileState + { + public string FirstName { get; set; } = ""; + public string LastName { get; set; } = ""; + public string Photo { get; set; } = ""; + public DateTime? BirthDate { get; set; } + public string AddressLine1 { get; set; } = ""; + public string AddressLine2 { get; set; } = ""; + public string Zip { get; set; } = ""; + public string City { get; set; } = ""; + public string Mobile { get; set; } = ""; + public string Phone { get; set; } = ""; + public string CountryIso2 { get; set; } = ""; + public string StateCode { get; set; } = ""; + public Sex Sex { get; set; } + public string NationalityIso2 { get; set; } = ""; + public string MainResidenceIso2 { get; set; } = ""; + + // Walker Profile Properties + public string About { get; set; } = ""; + public double Radius { get; set; } + public DogWalkRequestType AllowedRequests { get; set; } + public bool PuppyAllowed { get; set; } + public bool DifficultAllowed { get; set; } + public DogAggressionLevel AggressionLevel { get; set; } + public DogSize AcceptedSize { get; set; } + public decimal PriceWalk { get; set; } + public decimal PriceDay { get; set; } + public decimal PriceSitting { get; set; } + public bool NotAvailable { get; set; } + public string NotAvailableInfo { get; set; } = ""; + } + + private OriginalWalkerProfileState _originalState; + /// /// Soll der Bottomsheet angezeigt werden? /// @@ -210,12 +264,16 @@ namespace gehGassiApp.ViewModels var states = _countryService.GetStatesAsync(value.Iso2).Result; States = states.ToObservableCollection(); } + + CheckForChanges(); } partial void OnSelectedStateChanged(State value) { if (value != null) AddressSelectionValid = true; + + CheckForChanges(); } partial void OnBirthDateChanged(DateTime? value) @@ -224,6 +282,75 @@ namespace gehGassiApp.ViewModels return; if (value.HasValue) BirthDay = value.Value.ToString("d", CultureInfo.CurrentCulture); + + CheckForChanges(); + } + + partial void OnSelectedSexChanged(SexItem value) + { + CheckForChanges(); + } + + partial void OnSelectedNationalityChanged(Country value) + { + CheckForChanges(); + } + + partial void OnSelectedMainResidenceChanged(Country value) + { + CheckForChanges(); + } + + partial void OnAppUserChanged(AppUser value) + { + // Entferne alte Event-Handler falls vorhanden + if (_appUser != null) + { + _appUser.PropertyChanged -= OnAppUserPropertyChanged; + if (_appUser.Address != null) + _appUser.Address.PropertyChanged -= OnAppUserPropertyChanged; + if (_appUser.Contact != null) + _appUser.Contact.PropertyChanged -= OnAppUserPropertyChanged; + } + + // Registriere neue Event-Handler + if (value != null) + { + value.PropertyChanged += OnAppUserPropertyChanged; + if (value.Address != null) + value.Address.PropertyChanged += OnAppUserPropertyChanged; + if (value.Contact != null) + value.Contact.PropertyChanged += OnAppUserPropertyChanged; + } + + CheckForChanges(); + } + + partial void OnWalkerProfileChanged(DogWalkerProfile value) + { + // Entferne alte Event-Handler falls vorhanden + if (_walkerProfile != null) + { + _walkerProfile.PropertyChanged -= OnWalkerProfilePropertyChanged; + } + + // Registriere neue Event-Handler + if (value != null) + { + value.PropertyChanged += OnWalkerProfilePropertyChanged; + } + + CheckForChanges(); + } + + partial void OnIsNewPhotoChanged(bool value) + { + CheckForChanges(); + } + + partial void OnPhotoPathChanged(string value) + { + CheckForChanges(); } #endregion @@ -656,11 +783,158 @@ namespace gehGassiApp.ViewModels var walkerProfile = await _walkerProfileService.GetAsync(App.CurrentUser.AccessToken, cts.Token, false); WalkerProfile = walkerProfile; + // Speichere ursprüngliche Werte für Change Detection + SaveOriginalValues(); + IsLoading = false; } + #region Event Handlers for Change Detection + + /// + /// Wird aufgerufen wenn sich eine Property des AppUser oder seiner Unterobjekte ändert + /// + private void OnAppUserPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + CheckForChanges(); + } + + /// + /// Wird aufgerufen wenn sich eine Property des WalkerProfile ändert + /// + private void OnWalkerProfilePropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + CheckForChanges(); + } + + #endregion + #region Helper + /// + /// Prüft ob sich Daten geändert haben + /// + private void CheckForChanges() + { + if (IsLoading || _originalState == null || AppUser == null) + return; + + try + { + // Prüfe AppUser Properties + var hasChanges = + AppUser.FirstName != _originalState.FirstName || + AppUser.LastName != _originalState.LastName || + AppUser.Photo != _originalState.Photo || + BirthDate != _originalState.BirthDate; + + // Prüfe Address Properties + if (AppUser.Address != null) + { + hasChanges = hasChanges || + (AppUser.Address.AddressLine1 ?? "") != _originalState.AddressLine1 || + (AppUser.Address.AddressLine2 ?? "") != _originalState.AddressLine2 || + (AppUser.Address.Zip ?? "") != _originalState.Zip || + (AppUser.Address.City ?? "") != _originalState.City; + } + + // Prüfe Contact Properties + if (AppUser.Contact != null) + { + hasChanges = hasChanges || + (AppUser.Contact.Mobile ?? "") != _originalState.Mobile || + (AppUser.Contact.Phone ?? "") != _originalState.Phone; + } + + // Prüfe Walker Profile Properties + if (WalkerProfile != null) + { + hasChanges = hasChanges || + (WalkerProfile.About ?? "") != _originalState.About || + WalkerProfile.Radius != _originalState.Radius || + WalkerProfile.AllowedRequests != _originalState.AllowedRequests || + WalkerProfile.PuppyAllowed != _originalState.PuppyAllowed || + WalkerProfile.DifficultAllowed != _originalState.DifficultAllowed || + WalkerProfile.AggressionLevel != _originalState.AggressionLevel || + WalkerProfile.AcceptedSize != _originalState.AcceptedSize || + WalkerProfile.PriceWalk != _originalState.PriceWalk || + WalkerProfile.PriceDay != _originalState.PriceDay || + WalkerProfile.PriceSitting != _originalState.PriceSitting || + WalkerProfile.NotAvailable != _originalState.NotAvailable || + (WalkerProfile.NotAvailableInfo ?? "") != _originalState.NotAvailableInfo; + } + + // Prüfe Selections + if (SelectedCountry?.Iso2 != _originalState.CountryIso2) + hasChanges = true; + if (SelectedState?.Code != _originalState.StateCode) + hasChanges = true; + if (SelectedSex?.Value != _originalState.Sex) + hasChanges = true; + if (SelectedNationality?.Iso2 != _originalState.NationalityIso2) + hasChanges = true; + if (SelectedMainResidence?.Iso2 != _originalState.MainResidenceIso2) + hasChanges = true; + + // Prüfe ob neues Foto vorhanden + if (IsNewPhoto && !string.IsNullOrWhiteSpace(PhotoPath)) + hasChanges = true; + + HasChanges = hasChanges; + } + catch (Exception) + { + // Fallback: Bei Fehlern Changes auf true setzen + HasChanges = true; + } + } + + /// + /// Speichert die ursprünglichen Werte für Change Detection + /// + private void SaveOriginalValues() + { + try + { + _originalState = new OriginalWalkerProfileState + { + FirstName = AppUser?.FirstName ?? "", + LastName = AppUser?.LastName ?? "", + Photo = AppUser?.Photo ?? "", + BirthDate = BirthDate, + AddressLine1 = AppUser?.Address?.AddressLine1 ?? "", + AddressLine2 = AppUser?.Address?.AddressLine2 ?? "", + Zip = AppUser?.Address?.Zip ?? "", + City = AppUser?.Address?.City ?? "", + Mobile = AppUser?.Contact?.Mobile ?? "", + Phone = AppUser?.Contact?.Phone ?? "", + CountryIso2 = SelectedCountry?.Iso2 ?? "", + StateCode = SelectedState?.Code ?? "", + Sex = SelectedSex?.Value ?? Sex.Undefined, + NationalityIso2 = SelectedNationality?.Iso2 ?? "", + MainResidenceIso2 = SelectedMainResidence?.Iso2 ?? "", + + // Walker Profile Properties + About = WalkerProfile?.About ?? "", + Radius = WalkerProfile?.Radius ?? 0, + AllowedRequests = WalkerProfile?.AllowedRequests ?? DogWalkRequestType.Walking, + PuppyAllowed = WalkerProfile?.PuppyAllowed ?? false, + DifficultAllowed = WalkerProfile?.DifficultAllowed ?? false, + AggressionLevel = WalkerProfile?.AggressionLevel ?? DogAggressionLevel.Low, + AcceptedSize = WalkerProfile?.AcceptedSize ?? DogSize.Small, + PriceWalk = WalkerProfile?.PriceWalk ?? 0, + PriceDay = WalkerProfile?.PriceDay ?? 0, + PriceSitting = WalkerProfile?.PriceSitting ?? 0, + NotAvailable = WalkerProfile?.NotAvailable ?? false, + NotAvailableInfo = WalkerProfile?.NotAvailableInfo ?? "" + }; + } + catch (Exception) + { + _originalState = new OriginalWalkerProfileState(); + } + } + /// /// Laden eines Fotos /// @@ -752,6 +1026,11 @@ namespace gehGassiApp.ViewModels IsNewPhoto = false; await Task.Delay(Core.Common.Constants.AnimationDelayDetails); await LoadAsync(); + + // Speichere Original-Werte für Change Detection + SaveOriginalValues(); + HasChanges = false; + IsBusy = false; _refresh = false; } diff --git a/gehGassiApp/gehGassiApp/Views/ProfileView.xaml b/gehGassiApp/gehGassiApp/Views/ProfileView.xaml index ee6a40e..2d6bdc5 100644 --- a/gehGassiApp/gehGassiApp/Views/ProfileView.xaml +++ b/gehGassiApp/gehGassiApp/Views/ProfileView.xaml @@ -19,10 +19,13 @@ - + + - - - + + + - + + + + + \ No newline at end of file diff --git a/gehGassiApp/gehGassiApp/Views/ProfileWalkerView.xaml b/gehGassiApp/gehGassiApp/Views/ProfileWalkerView.xaml index 4961678..1628aba 100644 --- a/gehGassiApp/gehGassiApp/Views/ProfileWalkerView.xaml +++ b/gehGassiApp/gehGassiApp/Views/ProfileWalkerView.xaml @@ -20,10 +20,13 @@ - + + @@ -992,33 +995,15 @@ Scale="1" VerticalOptions="Center" /> - - - + + + - + + + + +