背景
- 我有一些约束性的问题,我不能为我的生活解决。我已经花了近3天的工作,但我正式失去了,所以我希望有人能指出我在正确的方向。
- 这是MAUI项目的一部分,我设置它在不同的视图中显示一组对象。这些对象直接Map到SQL表定义,并且这些对象的每个示例都是通过对集成到SQL服务器的API进行REST调用来创建的。
- 为了减少代码重复并帮助保持一定的可维护性,我设置了两个不同的基本类型来表示每个SQL对象的
ContentViews
。 - 为了显示单个对象,我创建了一个
SqlObjectView<TObjectType>
的新示例,它被不同的对象视图继承。例如,UserAccountView
只接受泛型参数中的UserAccount
类型,XAML根据每个对象的属性而有所不同。 - 为了显示一个对象集合,我创建了一个
SqlCollectionView<TObjectType, TParentType>
的新示例,其中TObjectType
是我持有一个集合的对象类型,TParentType
是我从中获取这个集合的对象类型。 - 这是我第一次处理MAUI,但我对WPF绑定有很好的经验。虽然,从我所看到的,MAUI似乎是一个完全不同的动物。
- 请原谅我在这里忽略了任何重大的基本问题。我仍然在努力理解整个
BindingContext
概念。
问题
- 在我的主
ContentPage
中,我有一个包含几个子控件(ContentView
)的网格,这些子控件都直接绑定到UserAccount
示例。 - 此示例名为
SessionUser
。UserAccount
属性设置为在其值更改时触发PropertyChanged
事件。(这是 * 可能 * 我的问题开始的地方) - 在我第一次设置
ContentPage
上的网格中绑定到SessionUser
属性的每个子控件都可以很好地更新/绑定,但是在初始绑定之后,任何时候我都不会更新控件。
我的问题
- 如何强制刷新
SessionUser
属性的绑定,以相应地更新视图?如果不可能,为什么? - 有没有一种方法可以挂钩到
UserAccount
定义上的INotifyPropertyChanged事件处理程序,以便在SessionUser
中的某些内容更新时强制触发PropertyChanged
事件? - 为什么会这样?我有一种感觉,这与我的
BindingContext
有关,或者我正在为SessionUser
属性使用普通CLR属性而不是BindableProperty
。但即使我尝试这样设置,问题仍然存在
问题代码
- 我已经尝试在这里只包含相关的代码,但是如果缺少一些可以帮助澄清的东西,我可以提供它。
- 正如我之前提到的,绑定在第一次设置时工作得很好,但是当
SessionUser
属性上的属性被更改时,我无法捕获PropertyChanged
事件。 - 在xaml.cs代码段中也会有很多缺少的EventHandlers/Methods,因为其中大部分与此问题无关。
SpeedDooMainPage.xaml
和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
)
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
属性从CLR更改为BindableProperty
。 - 这并没有解决问题。子属性仍然没有通知拥有
ContentPage
。 - 我确实注意到我实际上击中了bindable属性,但只在第一次设置时击中了一次。
- 将新回调附加到
PropertyChanged
的SessionUser
事件。 - 这会导致执行中出现大量挂起。我觉得是通知太频繁了?
- 这也打破了我的ASP API项目不知何故。这需要我进一步调查。
- 需要时手动为每个子控件指定新值
- 正如预期的那样,这确实会强制刷新UI上的绑定,并正确显示内容。
- 但是,如果可能的话,我希望完全通过
BindingContext
和属性/事件来完成此操作
1条答案
按热度按时间xsuvu9jc1#
您可以尝试使用CommunityToolkit.Mvvm.ComponentModel中的ObservableProperty:
更改:
适用范围:
这将自动创建一个公共属性SessionUser。
查看文档以了解更多详细信息:ObservableProperty attribute