Reputation: 53
ContentViews
for each SQL object.
SqlObjectView<TObjectType>
which is inherited by the different object views. For example, the UserAccountView
just takes the type of UserAccount
in the generic parameter and the XAML varies for each object based on it's properties.SqlCollectionView<TObjectType, TParentType>
where TObjectType
is the type of object I'm holding a collection of, and TParentType
is the type of object I'm getting this collection from.BindingContext
concept.ContentPage
, I've got a grid with several child controls (ContentView
) which are all bound directly to a UserAccount
instance.SessionUser
. The UserAccount
property is setup to fire a PropertyChanged
event when the value is changed for it. (This is likely where my issue begins)ContentPage
that binds to the SessionUser
property is updating/binding perfectly fine the first time I set it, but anytime I set it after the initial binding does not update the controls.SessionUser
property to make the view update accordingly? If that's not possible, why?UserAccount
definition to force fire a PropertyChanged
event when something in the SessionUser
is updated?BindingContext
or the fact that I'm using a normal CLR property for the SessionUser
property instead of a BindableProperty
. But even when I tried setting it up that way, the issue persistedSessionUser
property are changed, I am not able to catch that PropertyChanged
event.SpeedDooMainPage.xaml
and SpeedDooMainPage.xaml.cs
SpeedDooMainPage.xaml
- (ContentPage
)
<?xml version="1.0" encoding="utf-8"?>
<ContentPage x:Class="SpeedDooUtility.SpeedDooMainPage"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:converters="using:SpeedDooUtility.ViewContent.Converters"
xmlns:notifications="clr-namespace:SpeedDooUtility.ViewContent.Notifications"
xmlns:sqlObjects="using:SpeedDooUtility.ViewContent.SqlModelViews.SqlObjects"
xmlns:sqlCollections="using:SpeedDooUtility.ViewContent.SqlModelViews.SqlCollections"
BindingContext="{Binding Source={RelativeSource Self}}">
<!-- Main Content Resources -->
<ContentPage.Resources>
<!-- ... -->
</ContentPage.Resources>
<!-- Main Page Content -->
<Grid VerticalOptions="Fill" HorizontalOptions="Fill" Margin="0">
<!-- User Login Content -->
<Border Style="{StaticResource ContentWrappingBorder}" VerticalOptions="Center"
HorizontalOptions="Center"
IsVisible="{Binding IsUserLoggedIn, Converter={StaticResource BooleanConverter}, ConverterParameter=true}">
<VerticalStackLayout VerticalOptions="Center" HorizontalOptions="Center" Spacing="15"
Padding="50">
<!-- ... -->
</VerticalStackLayout>
</Border>
<!-- Main Page Content -->
<Grid VerticalOptions="Fill" HorizontalOptions="Fill"
IsVisible="{Binding Path=IsUserLoggedIn, Converter={StaticResource BooleanConverter}, ConverterParameter=false}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".65*" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Style="{StaticResource ContentWrappingBorder}" Margin="5">
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="45" />
<RowDefinition Height="2.15*" />
<RowDefinition />
</Grid.RowDefinitions>
<Label Text="User And Vehicle Information"
Style="{StaticResource TitleTextStyle}" />
<Grid Grid.Row="1" Margin="2">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height=".685*" />
</Grid.RowDefinitions>
<Border Grid.Row="0" Style="{StaticResource SqlObjectBorder}">
<ScrollView>
<sqlObjects:UserAccountView
Padding="5,0,0,0"
SqlObjectInstance="{Binding Path=SessionUser}"
IsDeveloperMode="{Binding Path=IsDeveloperMode}" />
</ScrollView>
</Border>
<Border Grid.Row="1" Style="{StaticResource SqlObjectBorder}">
<ScrollView>
<sqlCollections:AuthTokenCollectionView
ParentSqlInstance="{Binding Path=SessionUser}"
IsDeveloperMode="{Binding Path=IsDeveloperMode}" />
</ScrollView>
</Border>
</Grid>
<Grid Grid.Row="2" Margin="2">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Style="{StaticResource SqlObjectBorder}">
<ScrollView>
<sqlCollections:VehicleCollectionView
x:Name="scvVehicles"
ParentSqlInstance="{Binding Path=SessionUser}"
IsDeveloperMode="{Binding Path=IsDeveloperMode}" />
</ScrollView>
</Border>
<Border Grid.Column="1" Style="{StaticResource SqlObjectBorder}">
<ScrollView>
<sqlCollections:FlashHistoryCollectionView
IsDeveloperMode="{Binding Path=IsDeveloperMode}"
ParentSqlInstance="{Binding Source={x:Reference scvVehicles}, Path=SelectedSqlInstance}" />
</ScrollView>
</Border>
</Grid>
</Grid>
</Border>
<!-- Flashing Controls -->
<Border Grid.Column="1" Style="{StaticResource ContentWrappingBorder}" Margin="5">
<!-- ... -->
</Border>
</Grid>
<notifications:ProgressBanner x:Name="ProgressBannerTop" BannerHeight="125"
Location="BannerTop" VerticalOptions="Start" />
<notifications:NotificationBanner x:Name="NotiBannerTop" BannerHeight="125"
Location="BannerTop" VerticalOptions="Start" />
</Grid>
</ContentPage>
SpeedDooMainPage.xaml.cs
- (ContentPage
)
namespace SpeedDooUtility
{
public partial class SpeedDooMainPage : ContentPage
{
#region Custom Events
// Custom events for processing specific actions/operations during execution
private readonly EventHandler<UserAccount> UserLoggedIn;
private void OnUserLoggedIn(object SendingObject, UserAccount LoggedInUser)
{
// Invoke the logged in event instance here and hide our UserLoginView page content
this.IsUserLoggedIn = true;
this.SessionUser = LoggedInUser;
}
protected bool SetField<T>(ref T Field, T FieldValue, [CallerMemberName] string PropertyName = null)
{
// See if we need to fire a new event for property changed or not
if (EqualityComparer<T>.Default.Equals(Field, FieldValue)) return false;
// Update our field value and fire our property changed event as needed
Field = FieldValue;
OnPropertyChanged(PropertyName);
return true;
}
#endregion //Custom Events
#region Fields
// Private backing fields for display/window configuration
private bool _isLoginReady; // Tells us if we're ready to login
private bool _isUserLoggedIn; // Sets if we're logged in or not
private bool _isDeveloperMode; // Sets if we're in developer mode or not
private UserAccount _sessionUser; // The currently logged in user account for this application
#endregion //Fields
#region Properties
// Public facing properties for the display/window configuration
public bool IsLoginReady
{
get => _isLoginReady;
private set => SetField(ref _isLoginReady, value);
}
public bool IsUserLoggedIn
{
get => _isUserLoggedIn;
private set => SetField(ref _isUserLoggedIn, value);
}
public bool IsDeveloperMode
{
get => _isDeveloperMode;
init => SetField(ref _isDeveloperMode, value);
}
public UserAccount SessionUser
{
get => _sessionUser;
private set => SetField(ref _sessionUser, value);
}
#endregion //Properties
#region Structs and Classes
#endregion //Structs and Classes
// ------------------------------------------------------------------------------------------------------------------------------------------
public SpeedDooMainPage()
{
// InitializeComponent and show this page content
InitializeComponent();
this.UserLoggedIn += this.OnUserLoggedIn;
}
// ------------------------------------------------------------------------------------------------------------------------------------------
private async void LoginUserButton_OnClicked(object SendingControl, EventArgs EventArgs)
{
try
{
// If the login is not ready return out
if (!IsLoginReady) return;
// Disable the sending button
Button SendingButton = (Button)SendingControl;
SendingButton.IsEnabled = false;
// Execute the login routine on a background thread to keep our UI alive
AuthToken LoginToken = null; Exception LoginException = null; UserAccount? UserGenerated = null;
if (!await Task.Run(() => this.ExecuteUserLogin(out UserGenerated, out LoginToken, out LoginException)))
{
// Return out since at this point login failed
SendingButton.IsEnabled = true;
return;
}
// Invoke the login event on the main window here
this.UserLoggedIn.Invoke(this, UserGenerated);
SendingButton.IsEnabled = true;
}
catch { return; }
}
private bool ExecuteUserLogin(out UserAccount? User, out AuthToken? LoginToken, out Exception LoginEx)
{
// Don't mind the logic inside this method. There's nothing involving the UI/XAML/BindingContext in here
}
}
}
SqlObjectView
- (ContentView
)SqlObjectView.cs
namespace SpeedDooUtility.ViewContent.SqlModelViews
{
public class SqlObjectView<TObjectType> : ContentView where TObjectType : SqlObject<TObjectType>
{
#region Custom Events
private static void OnViewTypeChanged(BindableObject BindableObj, object OldValue, object NewValue) { /* Callback Method */ }
private static void OnIsDeveloperModeChanged(BindableObject BindableObj, object OldValue, object NewValue) { /* Callback Method */ }
private static void OnSqlObjectInstanceChanged(BindableObject BindableObj, object OldValue, object NewValue)
{
// Store the view and check if the value needs to be updated
if (BindableObj is not SqlObjectView<TObjectType> SqlObjectViewInstance) return;
SqlObjectViewInstance.SqlObjectInstance = (TObjectType)NewValue;
SqlObjectViewInstance.OnPropertyChanged(nameof(SqlObjectInstance));
}
#endregion //Custom Events
#region Fields
// Public static bindings for property values on the view content
public static readonly BindableProperty ViewTypeProperty = BindableProperty.Create(
nameof(ViewType),
typeof(ViewTypes),
typeof(SqlObjectView<TObjectType>),
propertyChanged: OnViewTypeChanged);
public static readonly BindableProperty IsDeveloperModeProperty = BindableProperty.Create(
nameof(IsDeveloperMode),
typeof(bool),
typeof(SqlObjectView<TObjectType>),
propertyChanged: OnIsDeveloperModeChanged);
public static readonly BindableProperty SqlObjectInstanceProperty = BindableProperty.Create(
nameof(SqlObjectInstance),
typeof(TObjectType),
typeof(SqlObjectView<TObjectType>),
propertyChanged: OnSqlObjectInstanceChanged);
#endregion //Fields
#region Properties
// Public facing properties for our view content to bind onto
public ViewTypes ViewType
{
get => (ViewTypes)GetValue(ViewTypeProperty);
set => SetValue(ViewTypeProperty, value);
}
public bool IsDeveloperMode
{
get => (bool)GetValue(IsDeveloperModeProperty);
set => SetValue(IsDeveloperModeProperty, value);
}
public TObjectType SqlObjectInstance
{
get => (TObjectType)GetValue(SqlObjectInstanceProperty);
set => SetValue(SqlObjectInstanceProperty, value);
}
#endregion //Properties
#region Structs and Classes
/// <summary>
/// Enumeration holding our different types of view content for this control
/// </summary>
public enum ViewTypes { AuthToken, UserAccount, Vehicle, FlashHistory }
#endregion //Structs and Classes
// ------------------------------------------------------------------------------------------------------------------------------------------
protected SqlObjectView(ViewTypes View) { this.ViewType = View; }
}
}
SqlObjectCollectionView
- (ContentView
)SqlObjectCollectionView.cs
namespace SpeedDooUtility.ViewContent.SqlModelViews
{
public class SqlCollectionView<TObjectType, TParentType> : ContentView, INotifyCollectionChanged
where TObjectType : SqlObject<TObjectType>
where TParentType : SqlObject<TParentType>
{
#region Custom Events
// Event handler for invoking a new CollectionChanged event
public event NotifyCollectionChangedEventHandler CollectionChanged;
private static void OnViewTypeChanged(BindableObject BindableObj, object OldValue, object NewValue) { /* Callback Method */ }
private static void OnIsDeveloperModeChanged(BindableObject BindableObj, object OldValue, object NewValue) { /* Callback Method */ }
private static void OnParentSqlInstanceChanged(BindableObject BindableObj, object OldValue, object NewValue)
{
// Store the view and check if the value needs to be updated
if (BindableObj is not SqlCollectionView<TObjectType, TParentType> SqlCollectionInstanceView) return;
SqlCollectionInstanceView.ParentSqlInstance = (TParentType)NewValue;
SqlCollectionInstanceView.OnPropertyChanged(nameof(ParentSqlInstance));
// Check if we're using vehicle history or not and update flashes if needed
if (NewValue != null) SqlCollectionInstanceView.RefreshChildInstances();
else
{
// Clear our list if the vehicle is null and invoke a reset event for the collection
SqlCollectionInstanceView.SqlObjectInstances.Clear();
SqlCollectionInstanceView.OnCollectionChanged(NotifyCollectionChangedAction.Reset);
}
}
private static void OnSelectedSqlInstanceChanged(BindableObject BindableObj, object OldValue, object NewValue) { /* Callback Method */ }
private static void OnSqlObjectInstancesChanged(BindableObject BindableObj, object OldValue, object NewValue) { /* Callback Method */ }
// ======================================================================================================================
// NOTE: INotifyCollectionChanged is implemented, but I've left out those event handlers since they're working correctly
// ======================================================================================================================
#endregion //Custom Events
#region Fields
// Public static bindings for property values on the view content
public static readonly BindableProperty ViewTypeProperty = BindableProperty.Create(
nameof(ViewType),
typeof(ViewTypes),
typeof(SqlCollectionView<TObjectType, TParentType>),
propertyChanged: OnViewTypeChanged);
public static readonly BindableProperty IsDeveloperModeProperty = BindableProperty.Create(
nameof(IsDeveloperMode),
typeof(bool),
typeof(SqlCollectionView<TObjectType, TParentType>),
propertyChanged: OnIsDeveloperModeChanged);
public static readonly BindableProperty ParentSqlInstanceProperty = BindableProperty.Create(
nameof(SqlObjectInstances),
typeof(TParentType),
typeof(SqlCollectionView<TObjectType, TParentType>),
propertyChanged: OnParentSqlInstanceChanged);
public static readonly BindableProperty SelectedSqlInstanceProperty = BindableProperty.Create(
nameof(SqlObjectInstances),
typeof(TObjectType),
typeof(SqlCollectionView<TObjectType, TParentType>),
propertyChanged: OnSelectedSqlInstanceChanged);
public static readonly BindableProperty SqlObjectInstancesProperty = BindableProperty.Create(
nameof(SqlObjectInstances),
typeof(ObservableCollection<TObjectType>),
typeof(SqlCollectionView<TObjectType, TParentType>),
propertyChanged: OnSqlObjectInstancesChanged);
#endregion //Fields
#region Properties
// Public facing properties for our view content to bind onto
public ViewTypes ViewType
{
get => (ViewTypes)GetValue(ViewTypeProperty);
set => SetValue(ViewTypeProperty, value);
}
public bool IsDeveloperMode
{
get => (bool)GetValue(IsDeveloperModeProperty);
set => SetValue(IsDeveloperModeProperty, value);
}
public TParentType ParentSqlInstance
{
get => (TParentType)GetValue(ParentSqlInstanceProperty);
set => SetValue(ParentSqlInstanceProperty, value);
}
public TObjectType SelectedSqlInstance
{
get => (TObjectType)GetValue(SelectedSqlInstanceProperty);
set => SetValue(SelectedSqlInstanceProperty, value);
}
public ObservableCollection<TObjectType> SqlObjectInstances
{
get => (ObservableCollection<TObjectType>)GetValue(SqlObjectInstancesProperty);
set => SetValue(SqlObjectInstancesProperty, value);
}
#endregion //Properties
#region Structs and Classes
/// <summary>
/// Enumeration holding our different types of view content for this control
/// </summary>
public enum ViewTypes { Tokens, Vehicles, FlashHistories }
#endregion //Structs and Classes
// ------------------------------------------------------------------------------------------------------------------------------------------
/// <summary>
/// The base CTOR for a new SqlCollectionView. This configures development mode and sets up
/// what type of view we're using
/// </summary>
/// <param name="View">The type of view we're building</param>
protected SqlCollectionView(ViewTypes View)
{
// Store the view type and build a logger instance
this.ViewType = View;
this.SqlObjectInstances ??= new();
}
// ------------------------------------------------------------------------------------------------------------------------------------------
public void Add(TObjectType SqlInstance) { /* Adds to the SqlObjectInstances collection */ }
public void Update(TObjectType SqlInstance, bool AddMissing = false) { /* Updates value in the SqlObjectInstances collection */ }
public int Remove(TObjectType SqlInstance, bool MatchGUID = false) { /* Removes from the SqlObjectInstances collection */ }
// ------------------------------------------------------------------------------------------------------------------------------------------
protected void RefreshChildInstances()
{
// Find the parent type and make sure we can use it
Type ParentType = this.ParentSqlInstance.GetType();
if (ParentType != typeof(UserAccount) && ParentType != typeof(Vehicle))
throw new Exception($"Error! Can not use ParentType {ParentType.Name} for a SqlCollectionView!");
// Store and cast our parent object as it's requested type and populate the collection as needed
IEnumerable<TObjectType> LoadedChildObjects = new List<TObjectType>();
// ==================================================================================================
// NOTE: There's a big switch block here to store the new values for the LoadedChildObjects list here
// ==================================================================================================
// Clear our list of objects out and insert all of our new values one by one
this.SqlObjectInstances.Clear();
this.OnCollectionChanged(NotifyCollectionChangedAction.Reset);
// Iterate all the child objects and insert them into our collection
foreach (var ChildObject in LoadedChildObjects) this.Add(ChildObject);
this.SelectedSqlInstance = this.SqlObjectInstances.FirstOrDefault();
}
}
}
SessionUser
property from CLR to BindableProperty
.
ContentPage
.SessionUser
event for PropertyChanged
.
BindingContext
and properties/events if possibleUpvotes: 0
Views: 1326
Reputation: 66
You can try using an ObservableProperty from the CommunityToolkit.Mvvm.ComponentModel:
Change the:
private UserAccount _sessionUser;
public UserAccount SessionUser
{
get => _sessionUser;
private set => SetField(ref _sessionUser, value);
}
For:
using CommunityToolkit.Mvvm.ComponentModel;
[ObservableProperty] private UserAccount sessionUser;
This will automatically create a public property SessionUser.
Check the documentation for more details: ObservableProperty attribute
Upvotes: 0