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.

This commit is contained in:
Max Mannstein 2025-08-24 19:27:44 +02:00
parent 5c85ce545f
commit fb1756b82b
4 changed files with 564 additions and 53 deletions

View File

@ -159,6 +159,39 @@ namespace gehGassiApp.ViewModels
[ObservableProperty]
private Country _selectedMainResidence;
/// <summary>
/// Gibt an ob sich Daten geändert haben
/// </summary>
[ObservableProperty]
private bool _hasChanges;
/// <summary>
/// Original AppUser für Change Detection
/// </summary>
private AppUser _originalAppUser;
/// <summary>
/// State Class für Change Detection
/// </summary>
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
/// <summary>
/// Wird aufgerufen wenn sich eine Property des AppUser oder seiner Unterobjekte ändert
/// </summary>
private void OnAppUserPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
CheckForChanges();
}
#endregion
#region Helper
/// <summary>
/// Prüft ob sich Daten geändert haben
/// </summary>
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;
}
}
/// <summary>
/// Speichert die ursprünglichen Werte für Change Detection
/// </summary>
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();
}
}
/// <summary>
/// Laden eines Fotos
/// </summary>
@ -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;
}

View File

@ -172,6 +172,60 @@ namespace gehGassiApp.ViewModels
[ObservableProperty]
private Country _selectedMainResidence;
/// <summary>
/// Gibt an ob sich Daten geändert haben
/// </summary>
[ObservableProperty]
private bool _hasChanges;
/// <summary>
/// Original AppUser für Change Detection
/// </summary>
private AppUser _originalAppUser;
/// <summary>
/// Original WalkerProfile für Change Detection
/// </summary>
private DogWalkerProfile _originalWalkerProfile;
/// <summary>
/// Original State für Change Detection
/// </summary>
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;
/// <summary>
/// Soll der Bottomsheet angezeigt werden?
/// </summary>
@ -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
/// <summary>
/// Wird aufgerufen wenn sich eine Property des AppUser oder seiner Unterobjekte ändert
/// </summary>
private void OnAppUserPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
CheckForChanges();
}
/// <summary>
/// Wird aufgerufen wenn sich eine Property des WalkerProfile ändert
/// </summary>
private void OnWalkerProfilePropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
CheckForChanges();
}
#endregion
#region Helper
/// <summary>
/// Prüft ob sich Daten geändert haben
/// </summary>
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;
}
}
/// <summary>
/// Speichert die ursprünglichen Werte für Change Detection
/// </summary>
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();
}
}
/// <summary>
/// Laden eines Fotos
/// </summary>
@ -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;
}

View File

@ -19,10 +19,13 @@
</controls:BaseContentPage.TitleView>
<ContentPage.Content>
<Grid>
<AbsoluteLayout>
<!-- Main scrollable content (full screen) -->
<ScrollView
x:Name="rootScrollView"
Padding="20"
AbsoluteLayout.LayoutBounds="0,0,1,1"
AbsoluteLayout.LayoutFlags="All"
Padding="20,20,20,100"
HorizontalOptions="Fill"
IsVisible="{Binding IsNotLoading}">
<VerticalStackLayout
@ -485,30 +488,15 @@
Scale="1"
VerticalOptions="Center" />
<Button
x:Name="btnSave"
Command="{Binding SaveCommand}"
Text="{Static res:Text.Button_Done}">
<Button.IsEnabled>
<MultiBinding Converter="{StaticResource AllTrueMultiConverter}">
<Binding Path="IsValid" Source="{x:Reference firstNameValidation}" />
<Binding Path="IsValid" Source="{x:Reference lastNameValidation}" />
<Binding Path="IsValid" Source="{x:Reference birthdayValidation}" />
<Binding Path="IsValid" Source="{x:Reference addressLine1Validation}" />
<Binding Path="IsValid" Source="{x:Reference addressLine2Validation}" />
<Binding Path="IsValid" Source="{x:Reference zipValidation}" />
<Binding Path="IsValid" Source="{x:Reference cityValidation}" />
<Binding Path="IsValid" Source="{x:Reference mobileValidation}" />
<Binding Path="IsValid" Source="{x:Reference phoneValidation}" />
<Binding Path="AddressSelectionValid" />
<Binding Path="IsNotBusy" />
</MultiBinding>
</Button.IsEnabled>
</Button>
</VerticalStackLayout>
</ScrollView>
<VerticalStackLayout IsVisible="{Binding IsLoading}" VerticalOptions="Center">
<!-- Loading Indicator -->
<VerticalStackLayout
AbsoluteLayout.LayoutBounds="0,0,1,1"
AbsoluteLayout.LayoutFlags="All"
IsVisible="{Binding IsLoading}"
VerticalOptions="Center">
<ActivityIndicator
HorizontalOptions="Center"
IsRunning="{Binding IsLoading}"
@ -516,6 +504,35 @@
Scale="2"
VerticalOptions="Center" />
</VerticalStackLayout>
</Grid>
<!-- Floating Save Button (over content) -->
<Button
x:Name="btnSave"
Margin="0,0,0,20"
AbsoluteLayout.LayoutBounds="0.5,1,300,60"
AbsoluteLayout.LayoutFlags="PositionProportional"
Command="{Binding SaveCommand}"
HorizontalOptions="Center"
IsVisible="{Binding HasChanges}"
Text="{x:Static res:Text.Button_Done}"
VerticalOptions="End">
<Button.IsEnabled>
<MultiBinding Converter="{StaticResource AllTrueMultiConverter}">
<Binding Path="IsValid" Source="{x:Reference firstNameValidation}" />
<Binding Path="IsValid" Source="{x:Reference lastNameValidation}" />
<Binding Path="IsValid" Source="{x:Reference birthdayValidation}" />
<Binding Path="IsValid" Source="{x:Reference addressLine1Validation}" />
<Binding Path="IsValid" Source="{x:Reference addressLine2Validation}" />
<Binding Path="IsValid" Source="{x:Reference zipValidation}" />
<Binding Path="IsValid" Source="{x:Reference cityValidation}" />
<Binding Path="IsValid" Source="{x:Reference mobileValidation}" />
<Binding Path="IsValid" Source="{x:Reference phoneValidation}" />
<Binding Path="AddressSelectionValid" />
<Binding Path="IsNotBusy" />
</MultiBinding>
</Button.IsEnabled>
</Button>
</AbsoluteLayout>
</ContentPage.Content>
</controls:BaseContentPage>

View File

@ -20,10 +20,13 @@
</controls:BaseContentPage.TitleView>
<ContentPage.Content>
<Grid>
<AbsoluteLayout>
<!-- Main scrollable content (full screen) -->
<ScrollView
x:Name="rootScrollView"
Padding="20"
AbsoluteLayout.LayoutBounds="0,0,1,1"
AbsoluteLayout.LayoutFlags="All"
Padding="20,20,20,100"
HorizontalOptions="Fill"
IsVisible="{Binding IsNotLoading}"
VerticalOptions="FillAndExpand">
@ -992,33 +995,15 @@
Scale="1"
VerticalOptions="Center" />
<Button
x:Name="btnSave"
Command="{Binding SaveCommand}"
Text="{Static res:Text.Button_Done}">
<Button.IsEnabled>
<MultiBinding Converter="{StaticResource AllTrueMultiConverter}">
<Binding Path="IsValid" Source="{x:Reference firstNameValidation}" />
<Binding Path="IsValid" Source="{x:Reference lastNameValidation}" />
<Binding Path="IsValid" Source="{x:Reference birthdayValidation}" />
<Binding Path="IsValid" Source="{x:Reference addressLine1Validation}" />
<Binding Path="IsValid" Source="{x:Reference addressLine2Validation}" />
<Binding Path="IsValid" Source="{x:Reference zipValidation}" />
<Binding Path="IsValid" Source="{x:Reference cityValidation}" />
<Binding Path="IsValid" Source="{x:Reference mobileValidation}" />
<Binding Path="IsValid" Source="{x:Reference phoneValidation}" />
<Binding Path="AddressSelectionValid" />
<!--<Binding Source="{x:Reference priceWalkValidation}" Path="IsValid"></Binding>
<Binding Source="{x:Reference priceDayValidation}" Path="IsValid"></Binding>
<Binding Source="{x:Reference priceSittingValidation}" Path="IsValid"></Binding>-->
<Binding Path="IsNotBusy" />
</MultiBinding>
</Button.IsEnabled>
</Button>
</VerticalStackLayout>
</ScrollView>
<VerticalStackLayout IsVisible="{Binding IsLoading}" VerticalOptions="CenterAndExpand">
<!-- Loading Indicator -->
<VerticalStackLayout
AbsoluteLayout.LayoutBounds="0,0,1,1"
AbsoluteLayout.LayoutFlags="All"
IsVisible="{Binding IsLoading}"
VerticalOptions="CenterAndExpand">
<ActivityIndicator
HorizontalOptions="Center"
IsRunning="{Binding IsLoading}"
@ -1026,7 +1011,36 @@
Scale="2"
VerticalOptions="Center" />
</VerticalStackLayout>
</Grid>
<!-- Floating Save Button (over content) -->
<Button
x:Name="btnSave"
Margin="0,0,0,20"
AbsoluteLayout.LayoutBounds="0.5,1,300,60"
AbsoluteLayout.LayoutFlags="PositionProportional"
Command="{Binding SaveCommand}"
HorizontalOptions="Center"
IsVisible="{Binding HasChanges}"
Text="{x:Static res:Text.Button_Done}"
VerticalOptions="End">
<Button.IsEnabled>
<MultiBinding Converter="{StaticResource AllTrueMultiConverter}">
<Binding Path="IsValid" Source="{x:Reference firstNameValidation}" />
<Binding Path="IsValid" Source="{x:Reference lastNameValidation}" />
<Binding Path="IsValid" Source="{x:Reference birthdayValidation}" />
<Binding Path="IsValid" Source="{x:Reference addressLine1Validation}" />
<Binding Path="IsValid" Source="{x:Reference addressLine2Validation}" />
<Binding Path="IsValid" Source="{x:Reference zipValidation}" />
<Binding Path="IsValid" Source="{x:Reference cityValidation}" />
<Binding Path="IsValid" Source="{x:Reference mobileValidation}" />
<Binding Path="IsValid" Source="{x:Reference phoneValidation}" />
<Binding Path="AddressSelectionValid" />
<Binding Path="IsNotBusy" />
</MultiBinding>
</Button.IsEnabled>
</Button>
</AbsoluteLayout>
</ContentPage.Content>
<controls:BaseContentPage.BottomSheet>