转换器行为错过了 CollectionView 更新项目中的第一个项目

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

免责声明:我知道这不尊重 MVVM 模式,我故意扭曲它以在测试/调试时反映在 UI 中。

简单来说,我有第一个

CollectionView
通过每个项目的
ObservableCollection<MyStringVM>
(输入文本框)绑定到
Entry
(我的模型的视图模型表示)。 我有第二个
CollectionView
直接绑定到我的模型上的
DeepObservableCollection<MyString>
,以便在 UI 中直观地检查所有更改是否按模型中的预期传播(这是我扭曲 MVVM 模式)。 :

查看 XAML

<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>

查看csharp

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(视图模型)中的TwoThree
中提交(按下返回键盘)时,我看到它们反映了
在我的第二个
CollectionView
(模型)中。 但是 One 中的任何更改都不会得到反映,即使模型已正确更新(正如我通过
Trace.WriteLine(...)
调用在调试日志记录中发现的那样)。

在我的

Converter
中设置断点后,我发现在启动应用程序时,按照相同的方案调用了 9 次: 转换器被调用为

  1. a
    null
    value
    争论。
  2. a
    StringPOCO
    value
    带有“One”数据的参数。
  3. a
    StringPOCO
    value
    带有“One”数据的参数。
  4. a
    null
    value
    争论。
  5. a
    StringPOCO
    value
    带有“Two”数据的参数。
  6. a
    StringPOCO
    value
    带有“Two”数据的参数。
  7. a
    null
    value
    争论。
  8. a
    StringPOCO
    value
    带有“”数据的参数。
  9. a
    StringPOCO
    value
    带有“”数据的参数。

我正确地看到它们反映在用户界面中:

一个

两个

当我在第一个 CollectionView(视图模型)中更改 OneTwoThree

 中的任何一个时,在将更新传播到我的模型并返回到我的视图后,
Converter
被调用了 4 次。

2 次使用相应的“Two

StringPOCO
value
参数,2 次使用相应的“Three
StringPOCO
value
参数。

因此第一个对应的“One

StringPOCO
value
参数永远不会传递给
Converter
!??

这解释了为什么第一个条目永远不会在视觉上反映出来,但为什么转换器会出现这种错误???

感谢您在这篇长文章中关注我。

c# xaml maui converters collectionview
1个回答
0
投票

我可以完全重现这个问题。转换器不会在您第一次更改条目时做出反应,也不会在第一次条目时做出反应。有趣的是,您的代码在 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}"/>
...
© www.soinside.com 2019 - 2024. All rights reserved.