NotifyPropertyChange 已触发,但 UI 字段未在 Xamarin.Forms 中更新



我目前正在将一些抽象层重构到 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 代码段:

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

渲染器打开一个列表,允许用户选择他们想要的任何"TipoPasseio",并用DsPadrao(标准描述)填充文本框。一切正常,甚至对TipoPasseio的引用也在被选中后保留(我知道这一点,因为如果我第二次调出列表,它只会显示选定的DsPadrao,为用户提供清理它的选项。如果他这样做,第三次点击将再次显示所有选项。

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

有什么想法吗?

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

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

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

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

请注意关键字可观察。以下是我看到的 Xamarin 可以观察到的内容:

  • 数据上下文。但这不会改变。

就是这样。

关于MyViewModel.TipoPasseio属性的值,您发布的代码中没有任何内容显示此属性发生更改。但如果是这样,看起来MyViewModel并没有实现INotifyPropertyChanged,所以Xamarin没有办法观察到这样的变化。

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

根据您的陈述:

我没有看到myViewModel.TipoPasseio的二传手被召唤

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

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

换句话说,在 Xamarin 可以看到的所有内容中,唯一会通知它更改的内容是数据上下文。这似乎不是您的场景中正在发生变化的地方。其他任何值都不由INotifyPropertyChanged支持的属性持有。

如果没有完整的代码示例,就不可能确定正确的修复程序是什么。根据更改的内容和方式,您需要为当前未执行此操作的一个或多个类型实现INotifyPropertyChanged

事实证明,我没有发射正确对象的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事件,但现在我对这个解决方案感到满意。

最新更新