【问题标题】:NotifyPropertyChange fired but UI field not updated in Xamarin.FormsNotifyPropertyChange 已触发,但 Xamarin.Forms 中的 UI 字段未更新
【发布时间】:2019-08-12 21:00:10
【问题描述】:

我目前正在将一些抽象层重构到 Xamarin 应用程序中,以打破前一个开发人员留下的“单体”结构,但出现了问题。在我的 ViewModel 中,我有一些调用 NotifyPropertyChange 的属性,以便在从列表中选择值时更新 UI。像这样:

public Notifier : BindableObject, INotifyPropertyChanged
{
    //...
    protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

由于我的特殊需要,不得不创建一个中间层

public interface ISomeArea
{
    DefinicaoServicoMobile TipoPasseio { get; set; }
}

-

public class SomeAreaImpl : Notifier, ISomeArea
{
    //...
    protected DefinicaoServicoMobile _tipoPasseio;
    public DefinicaoServicoMobile TipoPasseio
    {
        get => _tipoPasseio;
        set
        {
            if (_tipoPasseio != value)
            {
                _tipoPasseio = value;
                NotifyPropertyChanged(nameof(TipoPasseio));
            }
        }
    }        
}

实际绑定的视图模型:

public MyViewModel : BaseViewModel, ISomeArea
{
    private SomeAreaImpl someArea;
    //...

    public MyViewModel()
    {
        // This is meant to provide interchangable areas across view models with minimal code replication
        someArea = new SomeAreaImpl(); 
    }

    public DefinicaoServicoMobile TipoPasseio 
    { 
        get => someArea.TipoPasseio; 
        set => someArea.TipoPasseio = value; 
    }

}

还有.xaml sn-p:

<renderers:Entry
    x:Name="TxtTipoPasseio"
    VerticalOptions="Center"
    HeightRequest="60"
    HorizontalOptions="FillAndExpand"
    Text="{Binding TipoPasseio.DsPadrao}"
/>

渲染器打开一个列表,允许用户选择他们想要的任何“TipoPasseio”,并用DsPadrao(标准描述)填充文本框。一切正常,即使对TipoPasseio 的引用在被选中后仍被保留(我知道这一点,因为我应该再次打开列表,它只会显示选定的DsPadrao,让用户可以选择清理它。如果他这样做了,第三次点击将再次显示所有选项。

我可能搞砸了抽象,因为我没有看到 myViewModel.TipoPasseio 的设置器被调用,tbh

有什么想法吗?

【问题讨论】:

    标签: xaml xamarin xamarin.forms data-binding


    【解决方案1】:

    让我们通过 Xamarin 所知道的来推理(尽我们所能,因为您没有包含所有相关代码):

    • 您有一个类型为MyViewModel 的数据上下文
    • 该视图模型对象有一个名为TipoPasseio 的属性,类型为DefinicaoServicoMobile
    • DefinicaoServicoMobile 类型有一个名为DsPadrao 的属性

    这是绑定到Entry.Text 属性的最后一个属性。

    在绑定中,对构成绑定源或路径的值的任何可观察到的更改都会导致运行时更新绑定的目标属性 (Entry.Text),从而导致视觉外观发生变化(即新正在显示的文本)。

    注意关键字可观察。以下是我看到的 Xamarin 可以观察到的东西:

    • 数据上下文。但这并没有改变。

    就是这样。

    关于MyViewModel.TipoPasseio 属性的值,您发布的代码中没有任何内容显示此属性发生变化。但如果确实如此,它看起来不像 MyViewModel 实现 INotifyPropertyChanged,因此 Xamarin 将无法观察到这种变化。

    关于第二点,您确实在 SomeAreaImpl 类型中实现了 INotifyPropertyChanged。但 Xamarin 对该对象一无所知。它没有引用它,因此无法订阅其PropertyChanged 事件。

    根据您的陈述:

    我没有看到 myViewModel.TipoPasseio 的设置器被调用

    这表明TipoPasseio 属性没有被更改。 IE。虽然您不会向 Xamarin 提供通知,即使它确实发生了变化,但它无论如何都不会发生变化。

    一个似乎正在改变的属性是DsPadrao 属性(毕竟,它实际上是为绑定提供值的属性)。虽然您没有提供足够的详细信息让我们确定,但似乎可以合理猜测 DefinicaoServicoMobile 没有实现 INotifyPropertyChanged,因此 Xamarin 无法找出它的价值属性也可能发生了变化。

    换句话说,在 Xamarin 可以看到的所有内容中,唯一会收到更改通知的是数据上下文。这似乎并不是您的情况发生的变化。由INotifyPropertyChanged 支持的属性不包含其他任何值。

    如果没有完整的代码示例,就不可能确定正确的解决方法是什么。根据正在发生的变化以及变化方式,您需要为当前未执行的一种或多种类型实现 INotifyPropertyChanged

    【讨论】:

      【解决方案2】:

      事实证明,我没有触发正确对象的NotifyPropertyChangedMyViewModelSomeAreaImpl 都在 Notifier 类中实现了 INotifyPropertyChanged,因为 BaseViewModel 也从 Notifier 扩展而来,但这最终在我的问题中被省略了。弄清楚这一点后,这是一个有效(且完整)的示例:

      public Notifier : BindableObject, INotifyPropertyChanged
      {
          //...
          protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
          {
              PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
          }
      }
      

      关于DefinicaoServicoMobile 的细节对于这个问题可以忽略不计

      public interface ISomeArea
      {
          //...
          DefinicaoServicoMobile TipoPasseio { get; set; }
          Task SetServico(ServicoMobile servicoAtual;
          //...
      }
      

      为了澄清

      public abstract class BaseViewModel : Notifier
      {
          protected abstract Task SetServico(ServicoMobile servicoAtual);
          public async Task SetServico()
          {
              //...
              await SetServico(servicoAtual);
              //...
          }
      }
      

      这里改变了一些东西。它不再从 Notifier 扩展,这开始有点奇怪。这也是我分配TipoPasseio

      的地方
      public class SomeAreaImpl : ISomeArea
      {
          //...
          protected DefinicaoServicoMobile _tipoPasseio;
          // I need to call the viewModel's Notifier, as this is the bound object
          private BaseViewModel viewModel;
      
          public AreaServicosDependentesImpl(BaseViewModel viewModel)
          {
              this.viewModel = viewModel;
          }
      
          public DefinicaoServicoMobile TipoPasseio
          {
              get => _tipoPasseio;
              set
              {
                  if (_tipoPasseio != value)
                  {
                      _tipoPasseio = value;
                      viewModel.NotifyPropertyChanged(nameof(TipoPasseio));
                  }
              }
          }
      
          //Assigning to the property
          public async Task SetServico(ServicoMobile servicoAtual, List<DefinicaoServicoMobile> listDefinicaoServico)
          {
              //...
              TipoPasseio = listDefinicaoServico
                  .FirstOrDefault(x => x.CdServico == servicoAtual.TpPasseio.Value);
              //...
          }
      
      }
      

      视图模型的变化:

      public MyViewModel : BaseViewModel, ISomeArea
      {
          private SomeAreaImpl someArea;
          //...
      
          public MyViewModel()
          {
              someArea = new SomeAreaImpl(this); 
          }
      
          public DefinicaoServicoMobile TipoPasseio 
          { 
              get => someArea.TipoPasseio; 
              set => someArea.TipoPasseio = value; 
          }
      
          protected override async Task SetServico(ServicoMobile servicoAtual)
          {
              //...
              someArea.SetServico(servicoAtual, ListDefinicaoServico.ToList());
              //...
          }
      
      }
      

      视图模型绑定

      public abstract class BaseEncerrarPontoRotaPage : BasePage
      {
      
          private Type viewModelRuntimeType;
      
          public BaseEncerrarPontoRotaPage(Type viewModelRuntimeType)
          {
              this.viewModelRuntimeType = viewModelRuntimeType;
          }
      
          private async Task BindContext(PontoRotaMobile pontoRota, ServicoMovelMobile servicoMovel, bool finalizar)
          {
              _viewModel = (BaseViewModel)Activator.CreateInstance(viewModelRuntimeType, new object[] { pontoRota, UserDialogs.Instance });
      
              //...
              await _viewModel.SetServico();
              //...
              BindingContext = _viewModel;
          }
      
          public static BaseEncerrarPontoRotaPage Create(EnumAcaoServicoType enumType)
          {
              Type pageType = enumType.GetCustomAttribute<EnumAcaoServicoType, PageRuntimeTypeAttribute>();
              Type viewModelType = enumType.GetCustomAttribute<EnumAcaoServicoType, ViewModelRuntimeTypeAttribute>();
              return (BaseEncerrarPontoRotaPage)Activator.CreateInstance(pageType, new object[] { viewModelType });
          }
      }
      

      页面实例化是在其他视图模型中执行的,与此处显示的结构无关

      private async Task ShowEdit(bool finalizar)
      {
          await Task.Run(async () => 
          {
              var idAcaoServico = ServicoMobileAtual.DefinicaoServicoMobile.IdAcaoServico;
              var page = BaseEncerrarPontoRotaPage.Create((EnumAcaoServicoType)idAcaoServico);
      
              await page.BindContext(PontoRotaAtual, ServicoMovelMobileAtual, finalizar);
      
              BeginInvokeOnMainThread(async () =>
              {
                  await App.Navigation.PushAsync(page);
              });
          });
      }
      

      代码隐藏:

      public partial class MyPage : BaseEncerrarPontoRotaPage
      {
          public NormalUnidadePage() { }
      
          public MyPage(Type viewModelType) : base(viewModelType)
          {
              InitializeComponent();
              //Subscription to show the list
              TxtTipoPasseio.Focused += TxtTipoPasseio_OnFocused;
              //...
          }
       }
      

      XAML

      <views:BaseEncerrarPontoRotaPage xmlns="http://xamarin.com/schemas/2014/forms"
               xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
               xmlns:views="clr-namespace:My.Name.Space.;assembly=Phoenix.AS"
               x:Class="My.Name.Space.MyPage">
      
               //...             
      
               <renderers:Entry
                  x:Name="TxtTipoPasseio"
                  VerticalOptions="Center"
                  HeightRequest="60"
                  HorizontalOptions="FillAndExpand"
                  Text="{Binding TipoPasseio.DsPadrao}"/>
      
               //...
      </views:BaseEncerrarPontoRotaPage>  
      

      我知道可以从 AreaImpl 类传播一个事件,以便在视图模型中触发 Notify 事件,但现在我对这个解决方案很满意。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2014-01-02
        • 1970-01-01
        • 2019-08-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多