ObservableCollection 上的 OnPropertyChanged 不会刷新 CollectionView 内容

问题描述 投票:0回答:1

问题

我的 ViewModel 中有一个 ItemsChangedObservableRangeCollection (它基本上是来自 MVVMHelpers 包的 ObservableRangeCollection,有一些额外的方法可以在集合中的项目更改时添加事件),并且在我的 Xaml 页面中有一个绑定到此集合的 CollectionView 。为了显示我的数据,我使用了一些转换器,当我第一次打开页面时,它们按预期工作。

当我稍后返回此页面并且我想刷新我的数据时,就会出现问题。尽管数据被清除和刷新,并且尽管在集合本身或包含的项目上调用了 OnPropertyChanged 事件(我什至尝试从添加用于调试的临时按钮手动调用它),但 CollectionView 并未更新,并且甚至没有调用转换器。

只是为了一点额外的上下文,我的 ObservableRangeCollection 中的数据实际上并没有改变:集合包含用户数据,转换器的作用是使用用户 ID 并获取一些可能会发生变化的分数;这就是为什么我尝试手动调用 OnPropertyChanged 来强制转换器刷新。

我的代码:

ContentPage 的摘录,其中包含 CollectionView 以及正在使用的不同转换器:

<ContentPage.Behaviors>
    <toolkit:EventToCommandBehavior Command="{Binding RefreshVMCommand}" EventName="Appearing"/>
</ContentPage.Behaviors>

<!-- [...] -->

<CollectionView x:Name="participantsListView" 
ItemsSource="{Binding CompetitorsBySelectedGroup }" 
SelectionMode="None" >
    <CollectionView.ItemTemplate>
        <DataTemplate>
            <Grid x:DataType="model:Competitor" HeightRequest="76" >
                
                <!--Grid Row and Column Definitions-->

                <Label Grid.Column="0" Text="{Binding Number }"/>
                <Label Grid.Column="1" Text="{Binding FullName }"/>

                <Image Grid.Column="2" Source="task_list_check.svg" IsVisible="{Binding ., Converter={StaticResource IsResultScoreNotNullConverter}}"/>

                <Border Grid.Column="3" IsVisible="{Binding ., Converter={StaticResource IsResultScoreNotNullConverter}}">
                    <Label Text="{Binding ., Converter={StaticResource GetResultScore}}"/>
                </Border>
            </Grid>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

用于刷新数据的代码,从页面 OnAppearing 上的 ViewModel 调用:

[RelayCommand]
public void RefreshVM()
{
    CompetitorsBySelectedGroup?.Clear();
    var competitorList = DataManager.Instance.AppData.GetCompetitorsByCurrentGroup();
    CompetitorsBySelectedGroup.AddRange(competitorList ?? []);

    // Tentative to notify collection changed
    OnPropertyChanged(nameof(CompetitorsBySelectedGroup));

    // Tentative to notify directly items changed
    foreach (INotifyPropertyChanged item in CompetitorsBySelectedGroup)
    {
        OnPropertyChanged(nameof(item));
    }
}

我注意到一些有趣的事情,当我没有清除我的 ObservableCollection (

CompetitorsBySelectedGroup?.Clear()
),而是重新定义它 (
CompetitorsBySelectedGroup = []
) 时,CollectionView 实际上会刷新,但出于某些寻址原因,我宁愿避免这样做。

我的 ItemsChangedObservableRangeCollection 的代码:

namespace MauiUtils
{
    /// <summary>
    ///     This class adds the ability to refresh the list when any property of
    ///     the objects changes in the list which implements the INotifyPropertyChanged. 
    /// </summary>
    /// <typeparam name="T"/>
    public class ItemsChangedObservableRangeCollection<T> : ObservableRangeCollection<T> where T : INotifyPropertyChanged
    {
        public delegate void ItemChangedEventHandler(object source, EventArgs args);

        /// <summary>
        /// Event fired when an item of the collection is updated
        /// </summary>
        public event ItemChangedEventHandler ItemChanged;

        protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Add)
            {
                RegisterPropertyChanged(e.NewItems);
            }
            else if (e.Action == NotifyCollectionChangedAction.Remove)
            {
                UnRegisterPropertyChanged(e.OldItems);
            }
            else if (e.Action == NotifyCollectionChangedAction.Replace)
            {
                UnRegisterPropertyChanged(e.OldItems);
                RegisterPropertyChanged(e.NewItems);
            }
            base.OnCollectionChanged(e);
        }

        protected override void ClearItems()
        {
            UnRegisterPropertyChanged(this);
            base.ClearItems();
        }

        private void RegisterPropertyChanged(IList items)
        {
            foreach (INotifyPropertyChanged item in items)
            {
                if (item != null)
                {
                    item.PropertyChanged += new PropertyChangedEventHandler(item_PropertyChanged);
                }
            }
        }

        private void UnRegisterPropertyChanged(IList items)
        {
            foreach (INotifyPropertyChanged item in items)
            {
                if (item != null)
                {
                    item.PropertyChanged -= new PropertyChangedEventHandler(item_PropertyChanged);
                }
            }
        }

        private void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            OnItemChange();
        }

        protected virtual void OnItemChange()
        {
            ItemChanged?.Invoke(this, EventArgs.Empty);
        }
    }
}
c# .net mvvm maui
1个回答
0
投票

仔细阅读您的帖子,似乎您可能会竭尽全力刷新“应该”自动刷新的属性。问题是, ObservableRangeCollection<Competitor> 仅在添加或删除项目时触发

CollectionChanged
事件。但是您的
DataTemplate
(其中
FullName
Number
是绑定属性)需要响应
items
中的属性更改,而不是列表中的更改。 <DataTemplate> <Grid x:DataType="model:Competitor" HeightRequest="76" > <Label Grid.Column="0" Text="{Binding Number }"/> <Label Grid.Column="1" Text="{Binding FullName }"/> <Image Grid.Column="2" Source="task_list_check.svg" IsVisible="{Binding ., Converter={StaticResource IsResultScoreNotNullConverter}}"/> <Border Grid.Column="3" IsVisible="{Binding ., Converter={StaticResource IsResultScoreNotNullConverter}}"> <Label Text="{Binding ., Converter={StaticResource GetResultScore}}"/> </Border> </Grid> </DataTemplate>

如果这些属性更改时刷新失败,那么怀疑就落在 
Competitor

类本身上,它必须实现

INotifyPropertyChanged
(通常通过继承社区工具包中的
ObservableObject
)。

public class Competitor : INotifyPropertyChanged // Or inherit ObservableObject { public string FullName { get => _fullName; set { if (!Equals(_fullName, value)) { _fullName = value; OnPropertyChanged(); } } } string _fullName = string.Empty; /// <summary> /// A string, to allow something like "RP-002916" /// </summary> public string Number { get => _number; set { if (!Equals(_number, value)) { _number = value; OnPropertyChanged(); } } } string _number = string.Empty; /// <summary> /// A string, for expediency for testing, allow direct formatted entry /// </summary> public string? Score { get => _score; set { if (!Equals(_score, value)) { _score = value; OnPropertyChanged(); OnPropertyChanged(nameof(IsScoreVisible)); } } } string? _score = string.Empty; public bool IsScoreVisible => !string.IsNullOrEmpty(_score); protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); public event PropertyChangedEventHandler? PropertyChanged; }
最小示例
这里有一个最小的 Android 示例来解释“自动刷新自身”的含义。首先,创建一个包含五个
Competitor

对象的集合视图,其中两个没有分数。然后在 [Casey Taylor] 卡上向左滑动以调用

MainPage.xaml
中定义的乐谱编辑器覆盖层。

collection view with test data 乐谱编辑器绑定到传递到

Competitor

命令的

EditScores
的属性,但乐谱本身除外,在这种情况下,会创建一个副本以便能够恢复。如果单击
Apply
按钮,我们会将预览值复制到实际的
Competitor.Score
中。

simple score editor

关于你的问题
ObservableCollection不会刷新CollectionView内容

,我的观点是,当我们返回到CollectionView时,不需要刷新任何东西。由于

INotifyPropertyChanged
Competitor
类的操作,这应该已经得到解决。如果它不令人耳目一新,这就是您想要查看的地方。

before and after automatic

测试代码
注意:我不明白哪里需要

IValueConverter 转换器来有条件地显示分数,因此这个基本示例不包含任何转换器。


查看

public partial class MainPage : ContentPage { public MainPage() => InitializeComponent(); protected override void OnAppearing() { base.OnAppearing(); } new MainPageBindingContext BindingContext => (MainPageBindingContext)base.BindingContext; protected override bool OnBackButtonPressed() { if(BindingContext.IsEditingScore) { BindingContext.IsEditingScore = false; return true; } return base.OnBackButtonPressed(); } }
型号
class MainPageBindingContext : INotifyPropertyChanged { public MainPageBindingContext() { EditScoreCommand = new Command<Competitor>(OnEditScore); ApplyCommand = new Command(OnApply); CancelCommand = new Command(OnCancel); } /// <summary> /// ObservableRangeCollection is used in OPs question, but for this /// sample there's no reason you couldn't simply use ObservableCollection /// </summary> public ObservableRangeCollection<Competitor> CompetitorsBySelectedGroup { get; } = new ObservableRangeCollection<Competitor>() { new Competitor{ FullName = "Alex Johnson", Number = "AJ-001234", Score = "85" }, new Competitor{ FullName = "Jamie Lee", Number = "JL-001235", Score = "90" }, new Competitor{ FullName = "Morgan Smith", Number = "MS-001236" }, new Competitor{ FullName = "Casey Taylor", Number = "CT-001237" }, new Competitor{ FullName = "Jordan Brown", Number = "JB-001238", Score = "80" } }; public ICommand EditScoreCommand { get; } private void OnEditScore(Competitor competitor) { IsEditingScore = true; // Work with a copy so that Cancel can leave original unchanged. ScorePreview = competitor.Score; EditTarget = competitor; } public ICommand ApplyCommand { get; private set; } private void OnApply(object o) { EditTarget.Score = ScorePreview; IsEditingScore = false; } public ICommand CancelCommand { get; private set; } private void OnCancel(object o) => IsEditingScore = false; public Competitor EditTarget { get => _editTarget; set { if (!Equals(_editTarget, value)) { _editTarget = value; OnPropertyChanged(); } } } Competitor _editTarget = default; public bool IsEditingScore { get => _isEditingScore; set { if (!Equals(_isEditingScore, value)) { _isEditingScore = value; OnPropertyChanged(); OnPropertyChanged(nameof(IsNotEditingScore)); } } } bool _isEditingScore = default; /// <summary> /// Editing value for score that can be reverted on cancel. /// </summary> public string? ScorePreview { get => _scorePreview; set { if (!Equals(_scorePreview, value)) { _scorePreview = value; OnPropertyChanged(); } } } string? _scorePreview = string.Empty; public bool IsNotEditingScore => !IsEditingScore; protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); public event PropertyChangedEventHandler? PropertyChanged; }
XAML
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:RefreshCompetitors" x:Class="RefreshCompetitors.MainPage" Shell.NavBarIsVisible="{Binding IsNotEditingScore}"> <ContentPage.BindingContext> <local:MainPageBindingContext x:Name="Page"/> </ContentPage.BindingContext> <Grid> <VerticalStackLayout Padding="5,0" Spacing="25"> <Image Source="dotnet_bot.png" HeightRequest="185" Aspect="AspectFit" /> <CollectionView ItemsSource="{Binding CompetitorsBySelectedGroup }" SelectionMode="None" BackgroundColor="Azure"> <CollectionView.ItemTemplate> <DataTemplate> <SwipeView Threshold="50"> <!-- Swipe Items Definition --> <SwipeView.RightItems> <SwipeItems Mode="Execute"> <SwipeItemView Command="{Binding EditScoreCommand, Source={x:Reference Page}}" CommandParameter="{Binding .}"> <Grid WidthRequest="50" BackgroundColor="Aqua" RowDefinitions="*,*"> <Label Text="✎" FontSize="18" TextColor="Black" VerticalTextAlignment="Center" HorizontalTextAlignment="Center"/> <Label Grid.Row="1" TextColor="Black" Text="Score" FontSize="10" VerticalTextAlignment="Center" HorizontalTextAlignment="Center" VerticalOptions="Center"/> </Grid> </SwipeItemView> </SwipeItems> </SwipeView.RightItems> <Grid Margin="10,5" BackgroundColor="White" ColumnDefinitions="Auto,Auto,*" RowDefinitions="Auto, Auto" MinimumHeightRequest="40" RowSpacing="4"> <Label MinimumWidthRequest="100" Padding="20,0,0,0" Grid.Column="0" VerticalOptions="Center" HorizontalTextAlignment="Start" Text="{Binding Number }"/> <Label MinimumWidthRequest="150" Padding="20,0,0,0" Grid.Column="1" VerticalOptions="Center" HorizontalTextAlignment="Start" Text="{Binding FullName }"/> <CheckBox IsVisible="{Binding IsScoreVisible}" Grid.Row="1" Grid.Column="0" IsChecked="True" HorizontalOptions="Start"/> <Border IsVisible="{Binding IsScoreVisible}" Grid.Row="1" MinimumWidthRequest="100" Grid.Column="1" StrokeThickness="4"> <Label Padding="20,0,0,0" VerticalOptions="Center" HorizontalTextAlignment="Start" Text="{Binding Score}"/> </Border> </Grid> </SwipeView> </DataTemplate> </CollectionView.ItemTemplate> </CollectionView> </VerticalStackLayout> <!--Score editor overlay--> <Grid IsVisible="{Binding IsEditingScore}" BackgroundColor="Black" RowDefinitions="60,50,*,Auto,Auto, 100"> <Label HorizontalTextAlignment="Start" Text="{Binding EditTarget.FullName}" TextColor="White" FontSize="Title" Padding="20,20,0,0"/> <Label HorizontalTextAlignment="Start" Text="{Binding EditTarget.Number}" TextColor="White" FontSize="Subtitle" Padding="20,20,0,0" Grid.Row="1"/> <Entry VerticalOptions="Center" Placeholder="Score or Other" WidthRequest="250" FontSize="Subtitle" BackgroundColor="White" TextColor="Black" Grid.Row="2" Text="{Binding ScorePreview}"/> <Button Text="Apply" WidthRequest="100" HeightRequest="40" CornerRadius="20" Grid.Row="3" Margin="0,10" Command="{Binding ApplyCommand}"/> <Button Text="Cancel" WidthRequest="100" HeightRequest="40" CornerRadius="20" Grid.Row="4" Margin="0,10" Command="{Binding CancelCommand}"/> </Grid> </Grid> </ContentPage>
	
© www.soinside.com 2019 - 2024. All rights reserved.