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

Sje*_*oet 5 c# mvvm xamarin xamarin.forms

背景信息

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

BindingContext="{Binding [SearchViewModel], Source={StaticResource ViewModelLocator}}"
Run Code Online (Sandbox Code Playgroud)

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

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

问题

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

每次我返回搜索页面并导航回搜索结果时,都会重建视图并订阅PropertyChanged视图。BindableProperties这些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)
    {
        ...
    }
}
Run Code Online (Sandbox Code Playgroud)

我的问题:

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

编辑; 附加导航信息

我有一个MainView TabbedPage,它创建SearchViewNavigationPage

public MainView()
{
    InitializeComponent();

    Children.Add(new NavigationPage(new SearchView())
    {
        Title = AppResources.Tab_Search,
        Icon = "tab_search"
    });
}
Run Code Online (Sandbox Code Playgroud)

SearchViewViewModelLocator创建时,有一个由本主题开头提到的使用 MVVMLight容器分配的单实例 ViewModel SimpleIoc

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

await _navigationService.NavigateTo(ViewModelLocator.PageKeyFileResults, searchResult);
Run Code Online (Sandbox Code Playgroud)

哪个功能看起来有点像这样:

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);
}
Run Code Online (Sandbox Code Playgroud)

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

<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>
Run Code Online (Sandbox Code Playgroud)

其中 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");
    }
}
Run Code Online (Sandbox Code Playgroud)

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Kei*_*ome 1

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

不,Binding班级会处理这件事。不是BindableProperty.

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

您看到这一点是因为您忘记了导航堆栈在内存中保存了页面列表。由于多个页面指向同一个 BindingContext,因此有多个观察者来观察变化。如果您不重用视图模型,则不会遇到此特定问题。

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

不会。如果确实有问题,请设置BindingContextnull页面消失时,然后在重新出现时恢复它。请记住,这仍然是有成本的,特别是当您的 UI 非常繁忙并且有大量由数据绑定控制的动态内容时。