自定义视图中的BindableProperty未取消订阅PropertyChanged

本文关键字:取消 PropertyChanged BindableProperty 视图 自定义 | 更新日期: 2023-09-27 17:51:24

背景信息

我正在XAML中开发Xamarin Forms(v4.1.3,在iOS上测试(应用程序,使用MVVM和View first方法;我使用MVVMLight的ViewModelLocator服务将单个实例ViewModels分配给视图:

BindingContext="{Binding [SearchViewModel], Source={StaticResource ViewModelLocator}}"

当导航到另一个页面时,我正在构建该页面的一个新实例,它每次都会收到相同的ViewModel实例。

var page = new SearchView();
var tabbedPage = Application.Current.MainPage as TabbedPage;
if (tabbedPage != null)
    await tabbedPage.CurrentPage.Navigation.PushAsync(page);

问题

我已经实现了一个自定义控件(视图?(,它应该以类似平铺的布局显示搜索结果。该控件是在从搜索NavigationPage导航到搜索结果ContentPage时创建的。

每次我返回搜索页面并导航回搜索结果时,都会重建视图,并订阅BindablePropertiesPropertyChanged。这些PropertyChanged事件从未取消订阅,因此每当我导航到搜索结果视图并更改绑定的ViewModel属性时,该事件就会被多次触发。

在下面的代码中,OnItemsPropertyChanged被多次触发,这取决于我从搜索视图导航到搜索结果视图的次数:

public class WrapLayout : Grid
{
    public static readonly BindableProperty ItemsProperty =
        BindableProperty.Create("Items", typeof(IEnumerable), typeof(WrapLayout), null, propertyChanged: OnItemsPropertyChanged);
    public IEnumerable Items
    {
        get { return (IEnumerable)GetValue(ItemsProperty); }
        set { SetValue(ItemsProperty, value); }
    }
    public WrapLayout()
    {
        ...
    }
    private static void OnItemsPropertyChanged(BindableObject bindable, object oldValue, object newValue)
    {
        ...
    }
}

我的问题:

  • BindableProperty不应该自己取消订阅PropertyChanged-Changing
  • 发生这种情况是因为我将视图与ViewModels关联和/或浏览页面的方式吗
  • 我应该自己处理取消订阅这些活动吗?如何处理

编辑;附加导航信息

我有一个MainView TabbedPage,它将SearchView创建为NavigationPage:

public MainView()
{
    InitializeComponent();
    Children.Add(new NavigationPage(new SearchView())
    {
        Title = AppResources.Tab_Search,
        Icon = "tab_search"
    });
}

SearchView在创建时,使用MVVMLight的SimpleIoc容器,具有由本主题开头提到的ViewModelLocator分配的单个实例ViewModel。

SearchView中的搜索命令被触发时,我向API发送一个请求,返回搜索结果。这些结果显示在另一个页面上,我从SearchView的ViewModel:导航到该页面

await _navigationService.NavigateTo(ViewModelLocator.PageKeyFileResults, searchResult);

哪个功能看起来有点像:

public async Task NavigateTo(string pagekey, object viewModelParameter)
{
    var constructor = _pagesByKey[pagekey].Constructor; //Gets the Func<Page> that simple creates the requested page, without using reflection.
    var page = constructor() as Page;
    var viewModel = page.BindingContext as BaseViewModel;
    if (viewModel != null)
        viewModel.Initialize(viewModelParameter);
    var tabbedPage = Application.Current.MainPage as TabbedPage;
    if (tabbedPage != null)
        await tabbedPage.CurrentPage.Navigation.PushAsync(page);
    else
        await Application.Current.MainPage.Navigation.PushAsync(page);
}

构建的页面看起来有点像:

<pages:BaseContentPage 
  xmlns="http://xamarin.com/schemas/2014/forms"
  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
  x:Class="Views.FileResultsView"
  xmlns:pages="clr-namespace:Views.Pages;assembly=Views"
  xmlns:controls="clr-namespace:Views.Controls;assembly=Views"
  BindingContext="{Binding [FileResultsViewModel], Source={StaticResource ViewModelLocator}}">
  <ScrollView>
    <controls:WrapLayout
      Items="{Binding SearchResults}" />
  </ScrollView>
</pages:BaseContentPage>

BaseContentPage所在位置:

public class BaseContentPage : ContentPage
{
    protected override void OnAppearing()
    {
        base.OnAppearing();
        MessagingCenter.Subscribe<DialogMessage>(this, "ShowDialog", (dialogMessage) =>
        {
            if (string.IsNullOrWhiteSpace(dialogMessage.AcceptButton))
                DisplayAlert(dialogMessage.Title, dialogMessage.Content, dialogMessage.CancelButton);
            else
                DisplayAlert(dialogMessage.Title, dialogMessage.Content, dialogMessage.AcceptButton, dialogMessage.CancelButton);
        });
    }
    protected override void OnDisappearing()
    {
        base.OnDisappearing();
        MessagingCenter.Unsubscribe<DialogMessage>(this, "ShowDialog");
    }
}

ViewModel基本上是这样的:

public class FileResultsViewModel : BaseViewModel
{
    private IEnumerable<ASRow> _searchResults;
    public IEnumerable<ASRow> SearchResults
    {
        get { return _searchResults; }
        set { Set(ref _searchResults, value); }
    }
    internal override void Initialize(object parameter)
    {
        base.Initialize(parameter);
        if (parameter is AdvancedSearchResponse)
        {
            var searchResults = parameter as AdvancedSearchResponse;
            SearchResults = new List<ASRow>(searchResults.Rows);
        }
    }
}

自定义视图中的BindableProperty未取消订阅PropertyChanged

  • BindableProperty不应该自己取消订阅PropertyChanged和-Changing吗?
    • 是的,应该。如果没有,那肯定是一个错误
  • 发生这种情况是因为我将视图与ViewModels关联和/或浏览页面的方式吗?
    • 这很可能也是一种选择,因为我还没有经历过你描述的行为。您需要共享更多的周围设置代码
  • 我应该自己处理取消订阅这些活动吗?如何处理?
    • 你很难总是控制取消订阅,因为大多数时候都是控制订阅事件(除非你自己做,在这种情况下,你总是有责任再次取消订阅(

虽然这很难看,但有时有必要找到一个快速的解决方法,在您的情况下,可以浏览xamarin如何保存更改委托的列表,并在出现的页面上手动取消订阅它们。

我希望这能回答你的问题。如果没有,欢迎发表评论。

更新

在你的情况下,我会调试你的页面库,并验证是否

  • OnDisappearing调用正确
  • 取消订阅后,您的处理程序不在了
  • (这很懒惰,但我通常在对事件进行子脚本之前取消脚本,只是为了确保不会发生这样的错误,因为如果你试图取消脚本未注册的处理程序,大多数EventManagement服务都不会抛出。(

至少这是你的问题最可能的原因。

  • BindableProperty不应该自己取消订阅PropertyChanged和-Changing吗

没有。Binding类负责处理此问题。不是BindableProperty

  • 发生这种情况是因为我将视图与ViewModels关联和/或浏览页面的方式吗

您之所以看到这种情况,是因为您忘记了导航堆栈会在内存中保存页面列表。由于多个页面指向同一BindingContext,因此存在多个更改的观察者。如果不重用视图模型,就不会有这个特殊的问题。

  • 我应该自己处理取消订阅这些活动吗?如何处理

没有。如果确实存在问题,请在页面消失时将BindingContext设置为null,然后在再次出现时将其恢复。但请记住,这仍然有成本,尤其是当您的UI非常繁忙并且有大量由数据绑定控制的动态内容时。