我的 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()
) 时,CollectionView 实际上会刷新,但出于某些寻址原因,我宁愿避免这样做。CompetitorsBySelectedGroup = []
我的 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);
}
}
}
仔细阅读您的帖子,似乎您可能会竭尽全力刷新“应该”自动刷新的属性。问题是, 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;
}
Competitor
对象的集合视图,其中两个没有分数。然后在 [Casey Taylor] 卡上向左滑动以调用
MainPage.xaml中定义的乐谱编辑器覆盖层。
Competitor
命令的
EditScores
的属性,但乐谱本身除外,在这种情况下,会创建一个副本以便能够恢复。如果单击 Apply
按钮,我们会将预览值复制到实际的 Competitor.Score
中。关于你的问题,我的观点是,当我们返回到CollectionView
时,不需要刷新任何东西。由于
INotifyPropertyChanged
对 Competitor
类的操作,这应该已经得到解决。如果它不令人耳目一新,这就是您想要查看的地方。测试代码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;
}
<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>