XAML 如何通知拥有对象MAUI应用程序中子属性的更改?

4dc9hkyq  于 2023-05-27  发布在  其他
关注(0)|答案(1)|浏览(262)

背景

  • 我有一些约束性的问题,我不能为我的生活解决。我已经花了近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示例。
  • 此示例名为SessionUserUserAccount属性设置为在其值更改时触发PropertyChanged事件。(这是 * 可能 * 我的问题开始的地方)
  • 在我第一次设置ContentPage上的网格中绑定到SessionUser属性的每个子控件都可以很好地更新/绑定,但是在初始绑定之后,任何时候我都不会更新控件。

我的问题

  • 如何强制刷新SessionUser属性的绑定,以相应地更新视图?如果不可能,为什么?
  • 有没有一种方法可以挂钩到UserAccount定义上的INotifyPropertyChanged事件处理程序,以便在SessionUser中的某些内容更新时强制触发PropertyChanged事件?
  • 为什么会这样?我有一种感觉,这与我的BindingContext有关,或者我正在为SessionUser属性使用普通CLR属性而不是BindableProperty。但即使我尝试这样设置,问题仍然存在

问题代码

  • 我已经尝试在这里只包含相关的代码,但是如果缺少一些可以帮助澄清的东西,我可以提供它。
  • 正如我之前提到的,绑定在第一次设置时工作得很好,但是当SessionUser属性上的属性被更改时,我无法捕获PropertyChanged事件。
  • 在xaml.cs代码段中也会有很多缺少的EventHandlers/Methods,因为其中大部分与此问题无关。

SpeedDooMainPage.xamlSpeedDooMainPage.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属性从CLR更改为BindableProperty
  • 这并没有解决问题。子属性仍然没有通知拥有ContentPage
  • 我确实注意到我实际上击中了bindable属性,但只在第一次设置时击中了一次。
  • 将新回调附加到PropertyChangedSessionUser事件。
  • 这会导致执行中出现大量挂起。我觉得是通知太频繁了?
  • 这也打破了我的ASP API项目不知何故。这需要我进一步调查。
  • 需要时手动为每个子控件指定新值
  • 正如预期的那样,这确实会强制刷新UI上的绑定,并正确显示内容。
  • 但是,如果可能的话,我希望完全通过BindingContext和属性/事件来完成此操作
xsuvu9jc

xsuvu9jc1#

您可以尝试使用CommunityToolkit.Mvvm.ComponentModel中的ObservableProperty:
更改:

private UserAccount _sessionUser;
public UserAccount SessionUser
{
    get => _sessionUser;
    private set => SetField(ref _sessionUser, value);
}

适用范围:

using CommunityToolkit.Mvvm.ComponentModel;

[ObservableProperty] private UserAccount sessionUser;

这将自动创建一个公共属性SessionUser。
查看文档以了解更多详细信息:ObservableProperty attribute

相关问题