ViewModel PropertyChangedEventHandler null When Invoked From MessageCenter Subscription - xamarin

In my ContentPage I subscribe with the MessageCenter waiting on an event to occur. When I receive that message I need to update my ViewModel that has a BindingContext to my ContentPage, like so:
Page
public class MyPage : ContentPage
{
public MyPage()
{
Model = new ViewModel();
MessagingCenter.Subscribe<Application>(Application.Current, "MyMessage", (sender) =>
{
Model.Activated = true;
});
// ...
Title = "My Page";
Content = stackLayout;
BindingContext = Model;
}
public ViewModel Model { get; private set; }
}
View Model
public class ViewModel : INotifyPropertyChanged
{
private bool _activated;
public event PropertyChangedEventHandler PropertyChanged;
public bool Activated
{
get { return _activated; }
set
{
_activated = value;
PropertyChanged(this, new PropertyChangedEventArgs(nameof(Activated)));
}
}
}
Whenever I try to set Model.Activated = true; from the message subscription, I get a null reference exception on the PropertyChangedEventHandler (PropertyChanged) in my ViewModel here:
PropertyChanged(this, new PropertyChangedEventArgs(nameof(Activated)));
I assume this is because the message center is running on a background thread or something.
How do I fix this?

Move the bindingcontext assignment before Messagecenter function

Related

DataBinding issue in Xamarin Forms

I have a new Xamarin Forms 5 app and I'm having trouble with data binding.
First, I display a message that tells the user how many items are in his list. Initially, this is 0. It's displayed by DisplayMessage property of the view model.
Then, the Init() method gets called and once the API call is finished, there are some items in MyList. I put break points to make sure that the API call works and I end up with some data in MyList property.
Because I change the value of message in my Init() method, I was expecting the message to change and display the number of items in the list but it's not changing even though I have some items in MyList.
I created a new ViewModel that looks like this:
public class MyViewModel : BaseViewModel
{
public List<MyItem> MyList { get; set; } = new List<MyItem>();
string message = "You have no items in your list... ";
public string DisplayMessage
{
get => message;
set
{
if(message == value)
return;
message = value;
OnPropertyChanged();
}
}
public async void Init()
{
var data = await _myService.GetData();
if(data.Count > 0)
message = $"You have {data.Count} items in your list!";
MyList = data;
}
}
My MainPage code behind looks like this:
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class MainPage : ContentPage
{
MyViewModel _vm;
MainPage()
{
InitializeComponent();
_vm = new MyViewModel();
this.BindingContext = _vm;
}
protected override void OnAppearing()
{
base.OnAppearing();
_vm.Init();
}
}
I didn't change anyting in the base view model, except I added my service and it looks like this:
public class BaseViewModel : INotifyPropertyChanged
{
public IMyApiService MyApi => DependencyService.Get<IMyApiService>();
bool isBusy = false;
public bool IsBusy
{
get { return isBusy; }
set { SetProperty(ref isBusy, value); }
}
string title = string.Empty;
public string Title
{
get { return title; }
set { SetProperty(ref title, value); }
}
protected bool SetProperty<T>(ref T backingStore, T value,
[CallerMemberName] string propertyName = "",
Action onChanged = null)
{
if (EqualityComparer<T>.Default.Equals(backingStore, value))
return false;
backingStore = value;
onChanged?.Invoke();
OnPropertyChanged(propertyName);
return true;
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
var changed = PropertyChanged;
if (changed == null)
return;
changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
I'd appreciatae someone telling me where my mistake is. Thanks.
Without seeing the Xaml, I can't 100% answer, but here are a couple of things I see:
You are setting the "message" through the field, not the property. Since you are setting the field directly the OnPropertyChanged event isn't firing so the UI isn't getting notified that the value has changed.
I am guessing you are binding "MyList" to some sort of CollectionView or something? If it's a readonly view, using a List is ok as the collection is never updated. However, if you plan on adding or removing items at runtime, it needs to be an "ObservableCollection" for the same reason as above, the UI isn't notified of new items in a List, but an ObservableCollection will notify the UI of changes to it, so it can update.
Is what Jason mentions above in his comment. The MyList property should be setup like the other properties with the OnPropertyChanged.

PropertyChanged is triggered, but the view is not updated

I'm changing the label in the class constructor and it works fine, the label is updated ("0"). I'm also trying to update the label when I click in a button, but it's not working ("X"). I noticed debugging that the label value is updated, PropertyChanged is triggered, but the view doesn't change.
public class HomeViewModel : ViewModelBase
{
string playerA;
public string PlayerA
{
get
{
return playerA;
}
set
{
playerA = value;
this.Notify("playerA");
}
}
public ICommand PlayerA_Plus_Command
{
get;
set;
}
public HomeViewModel()
{
this.PlayerA_Plus_Command = new Command(this.PlayerA_Plus);
this.PlayerA = "0";
}
public void PlayerA_Plus()
{
this.PlayerA = "X";
}
}
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void Notify(string propertyName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
The name of the parameter passed in your PropertyChangedEventArgs is wrong. You are using "playerA" but the name of the (public) property is "PlayerA" (uppercase "P"). Change this.Notify("playerA"); to this.Notify("PlayerA"); or even better:
Notify(nameof(PlayerA));
You can completely get rid of passing the name of the param by adding a [CallerMemberName] attribute to the Notify() method.
protected void Notify([CallerMemberName] string propertyName = null)
This allows you to just call Notify() without parameters and the name of the changed property will automatically be used.

SetBinding exception using Xamarin Forms

I was stuck in this exception when starting using Xamarin forms with MVVM implementation
Method not found: 'Xamarin.Forms.BindableObjectExtensions.SetBinding'.
It failed at the line var mainNav = new MainPage ()
public static Page GetMainPage ()
{
RegisterTypes ();
var mainNav = new MainPage ();
return mainNav;
}
Here is my code, I have remove the unrelated codes to keep it simple. As you can see, it is very basic, and I knew I must get something very basic wrong, but just can't figure it out.
Thanks in advance....
View
public class MainPage :ContentPage
{
public MainPage ()
{
BindingContext = new MainPageViewModel ();
var nameEntry = new Entry ();
nameEntry.SetBinding (Entry.TextProperty, "Name");
Content = new StackLayout
{
Spacing = 12,
Padding = 20,
VerticalOptions = LayoutOptions.Start,
Children = { nameEntry }
};
}
}
ViewModel
public class MainPageViewModel:BaseViewModel
{
private string _name;
public string Name
{
get
{
return _name;
}
set
{
_name = value;
OnPropertyChanged();
}
}
}
I tried your exact code in a new project (Xamarin 3.9) and it worked fine (I only tested Android).
I omitted the RegisterTypes() from GetMainPage() and implemented the BaseViewModel as follows:
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Maybe you just need to update your Xamarin?

Windows Phone Page navigation in MVVM

I am using MVVM in one of the app. I have created different project for Model, View and View Model.
I need to navigate to another XAML from ViewModel. I found some solution using MVVM light. Is there any way of implementing navigation from view model without using MVVM light.
Simple as that,
IF you want to navigate from page1 to page2,
private void MoveToPage2FromPage1()
{
NavigationService.Navigate(new Uri("/Page2.xaml", UriKind.Relative));
}
How to perform page navigation on Windows Phone 8
You can store current page url on a notify property of Shared ViewModel in App. After that, it is easy to catch the change of this url and navigate to the correct url by observing it.
public class AppViewModel : INotifyPropertyChanged
{
public string CurrentPageURL { get; set; }
private string _currentPageURL;
public string CurrentPageURL
{
get { return _currentPageURL;}
set
{
if (_currentPageURL==value)
return; // to prevent reload the same page.
_currentPageURL = value;
NotifyPropertyChangedCurrentPageURL
}
}
// INotifyPropertyChanged implementations
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
// Store in static singleton instance of AppViewModel
public class App : Application
{
private static Lazy<AppViewModel> _ViewModel=new Lazy<ViewModel>();
public static AppViewModel ViewModel { get { return _ViewModel.Value; } }
....
public App()
{
AppViewModel.PropertyChanged=(s,a) =>
{
if (a.PropertyName=="CurrentPageURL")
{
NavigationService.Navigate(new Uri(AppViewModel.CurrentPageURL, UriKind.Relative));
};
}
}
}
// Usage sample
public class Page1ViewModel
{
private btnMoveNextPage_Click(object s, EventHandler a) {
App.ViewModel.CurrentURL="~/Page2.xaml";
}
}

NavigationService throws NullReferenceException

Using MVVM Light, I'm trying to develop a rather simple WP7 application. I've run into a problem using the navigation service. I can navigate to a page, but after pressing the back button I can't navigate to the same page again. NavigationService throws a NullReferenceException.
I have implemented my navigation using Messaging from the GalaSoft.MvvmLight.Messaging namespace. All my views inherits from a customized PhoneApplicationPage base class that registrers a listener on "NavigationRequest":
public class PhoneApplicationPage : Microsoft.Phone.Controls.PhoneApplicationPage
{
public PhoneApplicationPage() : base()
{
Messenger.Default.Register<Uri>(this, "NavigationRequest", (uri) => NavigationService.Navigate(uri));
}
}
From my view models I post Uri's to this listener:
SendNavigationRequestMessage(new Uri("/View/AppSettingsView.xaml", UriKind.Relative));
Like i said, this works except when navigating after pressing the Back button.
Why is this and how can I solve it?
Is there a better way to implement navigation using MVVM Light?
I'm using MVVM Light as well. I have a class called PageConductor, which is based on what John Papa (Silverlight MVP) from Microsoft uses. Here's the PageConductor Service I use
public class PageConductor : IPageConductor
{
protected Frame RootFrame { get; set; }
public PageConductor()
{
Messenger.Default.Register<Messages.FrameMessage>(this, OnReceiveFrameMessage);
}
public void DisplayError(string origin, Exception e, string details)
{
string description = string.Format("Error occured in {0}. {1} {2}", origin, details, e.Message);
var error = new Model.Error() { Description = description, Title = "Error Occurred" };
Messenger.Default.Send(new Messages.ErrorMessage() { Error = error });
}
public void DisplayError(string origin, Exception e)
{
DisplayError(origin, e, string.Empty);
}
private void OnReceiveFrameMessage(Messages.FrameMessage msg)
{
RootFrame = msg.RootFrame;
}
private void Go(string path, string sender)
{
RootFrame.Navigate(new Uri(path, UriKind.Relative));
}
public void GoBack()
{
RootFrame.GoBack();
}
}
In my MainPage.xaml.cs constructor, I have this, which creates an instance of my ContentFrame in my PageConductor service.:
Messenger.Default.Send(new Messages.FrameMessage() { RootFrame = ContentFrame });
I then use dependency injection to instantiate an instance of my PageConductor Service into my MainPage ViewModel. Here is my MainViewModel class:
protected Services.IPageConductor PageConductor { get; set; }
public RelayCommand<string> NavigateCommand { get; set; }
public MainViewModel(Services.IPageConductor pageConductor)
{
PageConductor = pageConductor;
RegisterCommands();
}
private void RegisterCommands()
{
NavigateCommand = new RelayCommand<string>(
(source) => OnNavigate(source));
}
private void OnNavigate(string sender)
{
PageConductor.GoToView(sender, "main");
}
Notice the instance of my PageConductorService as a parameter in my MainViewModel constructor method. I pass this in via my ViewModelLocator:
private readonly TSMVVM.Services.ServiceProviderBase _sp;
public ViewModelLocator()
{
_sp = Services.ServiceProviderBase.Instance;
CreateMain(_sp);
}
#region MainPageViewModel
public static MainViewModel MainStatic
{
get
{
Services.ServiceProviderBase SP = Services.ServiceProviderBase.Instance;
if (_main == null)
{
CreateMain(SP);
}
return _main;
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance",
"CA1822:MarkMembersAsStatic",
Justification = "This non-static member is needed for data binding purposes.")]
public MainViewModel Main
{
get
{
return MainStatic;
}
}
public static void ClearMain()
{
_main.Cleanup();
_main = null;
}
public static void CreateMain(Services.ServiceProviderBase SP)
{
if (_main == null)
{
_main = new MainViewModel(SP.PageConductor);
}
}
#endregion
For further reference, my Messages.FrameMessage class is simply:
internal class FrameMessage
{
public Frame RootFrame { get; set; }
}
I've had no issues with forward/back buttons.

Resources