免责声明:我知道这不尊重 MVVM 模式,我故意扭曲它以在测试/调试时反映在 UI 中。
简单来说,我有第一个
CollectionView
通过每个项目的 ObservableCollection<MyStringVM>
(输入文本框)绑定到 Entry
(我的模型的视图模型表示)。
我有第二个 CollectionView
直接绑定到我的模型上的 DeepObservableCollection<MyString>
,以便在 UI 中直观地检查所有更改是否按模型中的预期传播(这是我扭曲 MVVM 模式)。 :
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="TestingDeepRefresh.MainPage"
xmlns:models="clr-namespace:TestingDeepRefresh.Sources.Models"
xmlns:viewmodels="clr-namespace:TestingDeepRefresh.Sources.ViewModels"
xmlns:converters="clr-namespace:TestingDeepRefresh.Sources.Converters">
<ContentPage.Resources>
<converters:StringPOCOToStringConverter x:Key="stringPOCOToString" />
</ContentPage.Resources>
<ScrollView>
<VerticalStackLayout
Padding="30,0"
Spacing="25">
<Image
Source="dotnet_bot.png"
HeightRequest="185"
Aspect="AspectFit"
SemanticProperties.Description="dot net bot in a race car number eight" />
<CollectionView x:Name="stringVMCollection"
BackgroundColor="Red"
Margin="10"
VerticalOptions="Center"
HorizontalOptions="Center"
ItemsSource="{Binding MyStringVMs}">
<CollectionView.Header>
<Label Text="My stringVMs :"/>
</CollectionView.Header>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="viewmodels:StringVM">
<Entry
MaxLength="30"
Placeholder="enter Name"
Text="{Binding Name}"
ReturnCommand="{Binding Source={Reference stringVMCollection}, Path=BindingContext.EditStringVMCommand}"
ReturnCommandParameter="{Binding .}"
/>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<Button
BackgroundColor="Aquamarine"
Text="Update!"
Command="{Binding UpdatingCommand}"/>
<CollectionView
BackgroundColor="Teal"
Margin="10"
HorizontalOptions="Center"
VerticalOptions="Center"
ItemsSource="{Binding MyStrings}">
<CollectionView.Header>
<Label Text="My Strings"/>
</CollectionView.Header>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:StringPOCO">
<Label Text="{Binding Converter={StaticResource stringPOCOToString}}"/>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</VerticalStackLayout>
</ScrollView>
</ContentPage>
public partial class MainPage : ContentPage
{
private DeepObservableCollection<StringPOCO> _myStrings = new();
public DeepObservableCollection<StringPOCO> MyStrings => _myStrings;
private ObservableCollection<StringVM> _myStringVMs = new();
public ObservableCollection<StringVM> MyStringVMs => _myStringVMs;
private static MainPage? _instance;
public static MainPage? Instance => _instance;
public ICommand UpdatingCommand => new Command(Update);
public ICommand EditStringVMCommand => new Command<StringVM>(EditStringVM);
public MainPage()
{
InitializeComponent();
BindingContext = this;
_instance = this;
_PopulateMyStringVMs();
}
private void _PopulateMyStringVMs()
{
Trace.WriteLine($"Calling {nameof(_PopulateMyStringVMs)}...");
_myStringVMs.Clear();
_myStringVMs.Add(StringVM.Create("One"));
_myStringVMs.Add(StringVM.Create("Two"));
_myStringVMs.Add(StringVM.Create("Three"));
}
private void Update(object o)
{
Trace.WriteLine($"Calling {nameof(Update)}...");
foreach (var stringVM in _myStringVMs)
{
stringVM.UpdateAssociatedPOCO();
}
foreach (var stringPOCO in _myStrings)
Trace.WriteLine(stringPOCO.DTO?.Data);
}
private void EditStringVM(StringVM stringVM)
{
Trace.WriteLine($"Calling {nameof(EditStringVM)}...");
stringVM.UpdateAssociatedPOCO();
}
}
public partial class StringVM : ObservableObject
{
[ObservableProperty]
string _name;
public Guid ID { get; init; }
public StringVM(string name, Guid id)
{
_name = name;
ID=id;
}
public StringVM(string name)
: this(name, Guid.NewGuid())
{ }
public static StringVM Create(string name)
{
var poco = StringPOCO.Create(name);
if (poco == default)
return new StringVM(name);
var mainPage = MainPage.Instance;
if (mainPage != null)
{
mainPage.MyStrings.Add(poco);
}
return new StringVM(name, poco.ID);
}
public StringPOCO? GetAssociatedPOCO()
{
var mainPage = MainPage.Instance;
if (mainPage == default)
return default;
return mainPage.MyStrings.FirstOrDefault(poco => poco.ID == ID);
}
public bool UpdateAssociatedPOCO()
{
var foundPOCO = GetAssociatedPOCO();
if (foundPOCO?.DTO?.Data == default)
return false;
foundPOCO.DTO.Data = Name;
return true;
}
}
public partial class StringPOCO : ObservableObject
{
[ObservableProperty]
StringDTO _dTO;
public Guid ID { get; init; }
public StringPOCO(StringDTO dto)
: this(dto, Guid.NewGuid())
{ }
public StringPOCO(StringDTO dto, Guid id)
{
DTO = dto;
ID = id;
dto.PropertyChanged += (sender, EventArgs) =>
{
OnPropertyChanged(nameof(DTO));
};
}
public StringPOCO(string dtoAsString)
: this(new StringDTO(dtoAsString))
{ }
public static StringPOCO Create(string dtoAsString)
{
return new StringPOCO(dtoAsString);
}
}
//////////////////////////////////////////////////
public partial class StringDTO : ObservableObject
{
[ObservableProperty]
string _data;
public StringDTO(string data)
{
_data = data;
}
}
DeepObservableCollection
只是一个在其每个项目上注册 PropertyChanged 事件以调用 Reset 事件的 ObservableCollection
。 (摘自这篇SO帖子):
public class DeepObservableCollection<T> : ObservableCollection<T>
where T : INotifyPropertyChanged
{
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)
{
///Launch an event Reset with name of property changed
base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
完成所有这些内部绑定后,
MyStrings
(模型)集合项目的 DTO.Data 中的任何更改都将反映在我的第二个CollectionView
中。
为了正确显示它们,我使用这个转换器
public class StringPOCOToStringConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
StringPOCO? stringPOCO = value as StringPOCO;
StringDTO? stringDTO = stringPOCO?.DTO;
if (stringDTO == null)
return $"Fail to convert {value?.GetType().Name ?? $"NULL {nameof(value)}"} as a {nameof(StringPOCO)} to a string.";
return stringDTO.Data;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
事情变得奇怪了。 对
MyStringsVM
视图模型集合中的第二个和第三个条目的任何更改都会得到反映,但不会反映第一个!??
当我更改相应的
Entry
(文本框)并在第一个CollectionView
(视图模型)中的Two或Three中提交(按下返回键盘)时,我看到它们反映了
在我的第二个CollectionView
(模型)中。
但是 One 中的任何更改都不会得到反映,即使模型已正确更新(正如我通过 Trace.WriteLine(...)
调用在调试日志记录中发现的那样)。
在我的
Converter
中设置断点后,我发现在启动应用程序时,按照相同的方案调用了 9 次:
转换器被调用为
null
value
争论。StringPOCO
value
带有“One”数据的参数。StringPOCO
value
带有“One”数据的参数。null
value
争论。StringPOCO
value
带有“Two”数据的参数。StringPOCO
value
带有“Two”数据的参数。null
value
争论。StringPOCO
value
带有“三”数据的参数。StringPOCO
value
带有“三”数据的参数。我正确地看到它们反映在用户界面中:
一个
两个
三
当我在第一个 CollectionView
(视图模型)中更改 One、Two 或 Three
Converter
被调用了 4 次。
2 次使用相应的“Two”
StringPOCO
value
参数,2 次使用相应的“Three”StringPOCO
value
参数。
因此第一个对应的“One”
StringPOCO
value
参数永远不会传递给Converter
!??
这解释了为什么第一个条目永远不会在视觉上反映出来,但为什么转换器会出现这种错误???
感谢您在这篇长文章中关注我。
我可以完全重现这个问题。转换器不会在您第一次更改条目时做出反应,也不会在第一次条目时做出反应。有趣的是,您的代码在 Android 上按预期工作。您可以在 GitHub 上提交问题,.
我找到的解决方法是更改 XAML 中的 Binding 表达式(以及 Converter 中的一些代码片段)。例如,如果你尝试这个,
绑定
DTO
属性,
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:StringPOCO">
<Label Text="{Binding DTO, Converter={StaticResource stringPOCOToString}}"/>
</DataTemplate>
</CollectionView.ItemTemplate>
然后更换转换器,
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
StringDTO? stringDTO = value as StringDTO;
if (stringDTO == null)
return $"Fail to convert {value?.GetType().Name ?? $"NULL {nameof(value)}"} as a {nameof(StringPOCO)} to a string.";
return stringDTO.Data;
}
它按预期工作。
或者甚至你可以绑定到
DTO.Data
,那么你甚至不再需要转换器了。
...
<Label Text="{Binding DTO.Data}"/>
...