makoshichi
makoshichi

Reputation: 2390

NotifyPropertyChange fired but UI field not updated in Xamarin.Forms

I'm currently refactoring a few abstraction layers into a Xamarin app in order to break the "monolithic" structure left by the previous dev, but something has gone awry. In my ViewModel, I have a few properties that call NotifyPropertyChange in order to update the UI whenever a value is picked from a list. Like so:

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

Had to create a middle layer due to my specific needs

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

The actual bound view model:

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

}

And the .xaml snippet:

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

The renderer opens a list allowing the user to choose whichever "TipoPasseio" they want, and supposedly fill the textbox with a DsPadrao (standard description). Everything works, even the reference to TipoPasseio is held after being selected (I know this because should I bring up the list a second time, it will only display the selected DsPadrao, giving the user the option to clean it. If he does, a third tap will show all the options again.

I might have screwed up in the abstraction, as I don't see the setter for myViewModel.TipoPasseio being called, tbh

Any ideas?

Upvotes: 1

Views: 1345

Answers (2)

makoshichi
makoshichi

Reputation: 2390

As it turns out, I wasn't firing the NotifyPropertyChanged of the correct object. Both MyViewModel and SomeAreaImpl implemented INotifyPropertyChanged per the Notifier class as BaseViewModel also extends from Notifier but that ended up ommited in my question. Having figured that out, here's an working (and complete) example:

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

Specifics about DefinicaoServicoMobile are negligible to this issue

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

For the sake of clarification

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

Changed a couple of things here. It no longer extends from Notifier, which was kinda weird to begin with. Also this is where I assign 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);
        //...
    }

}

Changes to the view model:

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());
        //...
    }

}

View model binding

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

Page instantiation is performed in some other view model, not related to the structure presented here

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

Codebehind:

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>  

I know could propagate an event from the AreaImpl classes in order to fire the Notify event in the view model, but right now I'm satisfied with this solution.

Upvotes: 0

Peter Duniho
Peter Duniho

Reputation: 70671

Let's reason through what Xamarin knows (as best as we can, since you didn't include all of the relevant code):

  • You have a data context having the type MyViewModel
  • That view model object has a property named TipoPasseio, having type DefinicaoServicoMobile
  • The type DefinicaoServicoMobile has a property named DsPadrao

It is that last property that is bound to the Entry.Text property.

In a binding, any observable changes to values forming the source or path for the binding will cause the runtime to update the target property for the binding (Entry.Text) and thus result in a change in the visual appearance (i.e. new text being displayed).

Note the key word observable. Here are the things I see which are observable by Xamarin:

  • The data context. But this doesn't change.

That's it.

With respect to the value of the MyViewModel.TipoPasseio property, there's nothing in the code you posted showing this property changing. But if it did, it doesn't look like MyViewModel implements INotifyPropertyChanged, so Xamarin wouldn't have a way to observe such a change.

On that second point, you do implement INotifyPropertyChanged in the SomeAreaImpl type. But Xamarin doesn't know anything about that object. It has no reference to it, and so has no way to subscribe to its PropertyChanged event.

Based on your statement:

I don't see the setter for myViewModel.TipoPasseio being called

That suggests that the TipoPasseio property isn't being changed. I.e. while you wouldn't be providing notification to Xamarin even if it did change, it's not changing anyway.

One property that does seem to be changing is the DsPadrao property (after all, it's the property that's actually providing the value for the binding). And while you don't provide enough details for us to know for sure, it seems like a reasonable guess that the DefinicaoServicoMobile doesn't implement INotifyPropertyChanged, and so there's no way for Xamarin to ever find out the value of that property might have changed either.

In other words, of all the things that Xamarin can see, the only one that it would be notified about of a change is the data context. And that doesn't seem to be what's changing in your scenario. None of the other values are held by properties backed by INotifyPropertyChanged.

Without a complete code example, it's impossible to know for sure what the right fix is. Depending on what's changing and how though, you need to implement INotifyPropertyChanged for one or more of your types that don't currently do so.

Upvotes: 1

Related Questions