Reputation: 2390
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
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
Reputation: 70671
Let's reason through what Xamarin knows (as best as we can, since you didn't include all of the relevant code):
MyViewModel
TipoPasseio
, having type DefinicaoServicoMobile
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:
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