【问题标题】:BindableProperty in custom view does not unsubscribe PropertyChanged自定义视图中的 BindableProperty 不会取消订阅 PropertyChanged
【发布时间】:2016-06-28 08:29:55
【问题描述】:

背景信息

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

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 吗?
  • 这是因为我将视图与 ViewModel 关联和/或浏览页面的方式造成的吗?
  • 我应该自己处理退订这些事件吗?如何处理?

编辑;其他导航信息

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

public MainView()
{
    InitializeComponent();

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

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

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);
        }
    }
}

【问题讨论】:

    标签: c# mvvm xamarin xamarin.forms


    【解决方案1】:
    • BindableProperty 是否应该自行取消订阅 PropertyChanged 和 -Changing?
      • 是的——应该。如果不是,那肯定是一个错误
    • 这是因为我将视图与 ViewModel 关联和/或浏览页面的方式造成的吗?
      • 这很可能也是一种选择,因为我还没有遇到您描述的行为。您需要分享更多周边设置代码。
    • 我应该自己处理取消订阅这些事件吗?如何处理?
      • 您很难始终控制取消订阅,因为大多数时候它将控制订阅事件(除非您自己这样做,在这种情况下,您始终有责任再次取消订阅)

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

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

    更新

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

    • 正确调用 OnDisappearing
    • 您的处理程序在取消订阅后消失了
    • (这很懒,但我通常在订阅之前取消订阅事件,以确保不会发生此类错误,因为如果您尝试取消订阅未注册的处理程序,大多数 EventManagement 服务都不会抛出。 )

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

    【讨论】:

    • 如果这不是默认行为,我确实想在构建计数器测量之前找到一个解决方案。我已将从 MainView 发生的整个导航附加到 SearchResult。也许你可以看看,也许找出可能的原因?
    • @hantoun 是的,我会看看我能做什么。现在要去休息了。是的 - 尝试获得适当的解决方案通常是最理想的选择:)
    • @hantoun 已更新,请随时验证是否修复了它
    • MessagingCenter 似乎正在订阅和取消订阅;即便如此,这个 MessagingCenter 实现还是全新的。当我修改我的票时,我注意到BaseContentPage 正在将自身传递给不符合 MVVM 准则的 ViewModel。我认为这可能导致了主要问题。在我实施 MessagingCenter 之后,问题没有任何改变。
    • @Sjeijoet 不幸的是,全新并不意味着它没有错误。我使用过 TemplatedView 并且对错误也有一些乐趣,只是被告知他们不会为了向后兼容性而更改它 atm,就在功能发布之后。对我来说,Xamarin.Forms 仍处于公开测试阶段:/ 只能希望 MS 能收拾这个低质量的烂摊子
    【解决方案2】:
    • BindableProperty 不应该自行取消订阅 PropertyChanged 和 -Changing 吗?

    没有。 Binding 类负责这一点。不是BindableProperty

    • 这是因为我将 View 与 ViewModel 关联和/或浏览页面的方式造成的吗?

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

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

    没有。如果确实需要考虑,则在页面消失时将BindingContext 设置为null,然后在重新出现时将其恢复。请记住,这仍然需要付出代价,尤其是当您的 UI 非常繁忙并且有大量由数据绑定控制的动态内容时。

    【讨论】:

    • 谢谢,在那种情况下我可能只使用单实例视图(虽然这不是 MVVM),或者只是重新设计整个架构......我猜导航堆栈保留的页面是我正在寻找的答案。
    猜你喜欢
    • 2020-01-30
    • 2021-02-11
    • 1970-01-01
    • 2011-04-30
    • 1970-01-01
    • 2019-02-11
    • 1970-01-01
    • 2022-11-30
    • 2021-08-19
    相关资源
    最近更新 更多