How can I communicate in my app with ViewModel? - xamarin.forms

How communicate in my app with my ViewModel?
I have this code, sleep and resume of my app
protected override void OnSleep()
{
MessagingCenter.Send<App, string>(this, "gotosleep", "savedata");
}
in my ViewModel I subscribe to the message, but it does not work. My message is never displayed.
public MyViewModel()
{
MessagingCenter.Subscribe<App, string>(this, "gotosleep", async (obj, item) =>
{
Console.WriteLine("HERE");
});
}

You should use the MyViewModel in ContentPage, then Subscribe of MessagingCenter will work.
public MainPage()
{
InitializeComponent();
// Use model in Content Page
MyViewModel viewModel = new MyViewModel();
}
However, I find this does not work on iOS device, but works on Android.
Here is the Workaround to solve this, you can pass the ContentPage as an attribute for ViewModel when initialiation.
public MyViewModel(MainPage mainPage)
{
MessagingCenter.Subscribe<App, string>(mainPage, "gotosleep", async (obj, item) =>
{
Console.WriteLine("HERE");
await mainPage.DisplayAlert("Message received", "arg=" + item, "OK");
});
}
In ContentPage, modify as follows:
public MainPage()
{
InitializeComponent();
// Use model in Content Page
MyViewModel viewModel = new MyViewModel(this);
}
iOS effect:
Android effect:

Related

Xamarin forms Back Button Navigation

I'm working on a Xamarin Forms app and am using the MVVM Design.
the issue is when am navigating to another page using
Shell.Current.GoToAsync()
I disable the button to prevent Creating Multiple Pages or DB Operations.
but if I want to go back, I re-enable the buttons in the VM constructor, but the constructor never gets called which means the buttons are still disabled.
I tried to append the // in the Page route to remove the stack thinking that when I go back it will create a new instance Page and VM, but that did not work.
so can anyone help me resolving this problem.
thanks in advance.
Update:
VM Code
public RegisterViewModel()
{
Debug.WriteLine("Class Constructor", Class_Name);
//in case if disabled
RegisterButtonEnabled = true;
RegisterCommand = new Command(RegisterButtonOnClick);
}
public ICommand RegisterCommand { get; }
private bool registerButtonEnabled = true;
public bool RegisterButtonEnabled
{
get => registerButtonEnabled;
set
{
registerButtonEnabled = value;
OnPropertyChanged();
}
}
private async void RegisterButtonOnClick()
{
RegisterButtonEnabled = false;
//More Code
//and then go to Register Page
await Shell.Current.GoToAsync(nameof(RegisterPage));
}
and my xaml
<Button
Command="{Binding RegisterCommand}"
Text="{xct:Translate Register}"
Style="{StaticResource ButtonStyle}"
IsEnabled="{Binding RegisterButtonEnabled,Mode=OneWay}"/>
I had create a default shell project. And find something about the viewmodel. You can add the onappear and the ondisappear method to the viewmodel. Such as:
ViewModel:
public void OnAppearing()
{
RegisterButtonEnabled = true;
}
public void OnDisAppearing()
{
RegisterButtonEnabled = false;
}
Page.cs
ItemsViewModel _viewModel;
public ItemsPage()
{
InitializeComponent();
BindingContext = _viewModel = new ItemsViewModel();
}
protected override void OnAppearing()
{
base.OnAppearing();
_viewModel.OnAppearing();
}
protected override void OnDisappearing()
{
base.OnDisappearing();
_viewModel.OnDisAppearing();
}

Xamarin Forms: Display error occurring in view model

I'm following tutorials/examples for a Xamarin Forms project, where there is a view with a C# code-behind, binding to a view model. However, I want to catch an exception occurring in the view model and display it in an alert or use any other common technique for displaying errors.
Here is the view, which reloads data using a refresh:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyCompany.App.Views.DashboardPage"
xmlns:vm="clr-namespace:MyCompany.App.ViewModels"
xmlns:dashboard="clr-namespace:MyCompany.App.ViewModels.Dashboard;assembly=MyCompany.App"
Title="{Binding Title}">
...
<RefreshView x:DataType="dashboard:DashboardViewModel" Command="{Binding LoadItemsCommand}" IsRefreshing="{Binding IsBusy, Mode=TwoWay}">
... content
</RefreshView>
</ContentPage>
Then I have the C# code behind for the XAML:
public partial class DashboardPage : ContentPage
{
DashboardViewModel _viewModel;
public DashboardPage()
{
InitializeComponent();
BindingContext = _viewModel = new DashboardViewModel();
}
protected override void OnAppearing()
{
base.OnAppearing();
_viewModel.OnAppearing();
}
}
And finally the view model where the loading happens. It inherits from the BaseViewModel which is provided in the tutorials.
public class DashboardViewModel : BaseViewModel
{
private DashboardItemViewModel _selectedItem;
public ObservableCollection<DashboardItemViewModel> Items { get; }
public Command LoadItemsCommand { get; }
public Command<DashboardItemViewModel> ItemTapped { get; }
public DashboardViewModel()
{
Title = "Dashboard";
Items = new ObservableCollection<DashboardItemViewModel>();
LoadItemsCommand = new Command(async () => await ExecuteLoadItemsCommand());
ItemTapped = new Command<DashboardItemViewModel>(OnItemSelected);
}
async Task ExecuteLoadItemsCommand()
{
IsBusy = true;
try
{
Items.Clear();
var items = await GetItems();
foreach (var item in items)
{
Items.Add(item);
}
}
finally
{
IsBusy = false;
}
}
private static async Task<List<DashboardItemViewModel>> GetItems()
{
// Where errors happen
return items;
}
public void OnAppearing()
{
IsBusy = true;
SelectedItem = null;
}
public DashboardItemViewModel SelectedItem
{
get => _selectedItem;
set
{
SetProperty(ref _selectedItem, value);
OnItemSelected(value);
}
}
async void OnItemSelected(DashboardItemViewModel item)
{
if (item == null || item.Uri.IsNotSet())
return;
await Shell.Current.GoToAsync(item.Uri);
}
}
I can't see any overridable methods in ContentPage for catching exceptions. What's the best way to catch an exception and display it in an alert?
I'm not sure what exactly you want, but for catching errors I use try/catch method.
try
{
//your code here
}
catch(Exception exc)
{
await App.Current.MainPage.DisplayAlert("Warning", "Error: " + exc, "Ok");
}

Blazor state not updatting between child components when using events

I have two independent components in a Blazor wasm app between whichi am trying to communicate. under certain cases the communication fails and I cannot understand why.
The(simplified) setup is as follows
<ParentComponent>
<HeaderComponent>
<ProgressBar IsLoading="<Set by IsLoading property from header>" />
</HeaderComponent>
<ResultContainer />
</ParentComponent>
The code behind looks something like this:
public class ResultContainerStateManager
{
public event Action OnLoadStart;
public event Action OnLoadFinish;
public NotifyLoadStart() => this.OnLoadStart?.Invoke();
public NotifyLoadFinish() => this.OnLoadFinish?.Invoke();
}
public partial class HeaderComponent
{
[Inject]
public ResultContainerStateManager ResultContainerStateManager { get; set; }
private bool IsLoading { get; set; }
protected override void OnInitialized()
{
this.ResultContainerStateManager.OnLoadStart += () => this.IsLoading = true;
this.ResultContainerStateManager.OnLoadFinish += () => this.IsLoading = false;
base.OnInitializer();
}
}
public partial class ResultContainer
{
[Inject]
public ResultContainerStateManager ResultContainerStateManager { get; set; }
private bool IsLoading { get; set; }
protected override async Task OnParametersSetAsync()
{
<code>
if (shouldLoadData)
{
this.ResultContainerStateManager.NotifyLoadStart();
<more code>
this.ResultContainerStateManager.NotifyLoadFinish();
}
await base.OnParametersSetAsync();
}
}
public partial class ProgressBar
{
[Parameter]
public bool IsLoading { get; set; }
}
Where the IsLoading parameter from the progress bar is set from the IsLoading property from HeaderComponent, like
<div id="headerComponent">
<More html here>
<ProgressBar IsLoading="#this.IsLoading" />
</div>
I don't think it matters, but the progress bar itself uses the MatProgress component, like so:
#if (this.IsLoading)
{
<MatProgress Indeterminate="true" />
}
else
{
<Other html code>
}
The problem is that the progress bar starts when the ResultContainer executes the NotifyLoadStart() method, but it doesn't stop when the NotifyLoadFinish() method is executed.
I can see when debugging that the IsLoading property of the HeaderComponent is set back to false after the NotifyLoadFinish() call, but it has no Effect on the UI.
What I have tried so far:
injecting the ResultContainerStateManager directly into the Progress bar
I have tried changing the envents to Func and handling at all asynchronously
I have tried adding await Task.Yield() after each Notify call
I have tried adding this.StateHasChanged() calls in the event handlers and after each Notify call (I know the latter should not change anything at all, since it is not in the same hierarchy)
None of that changed anything and I would really like to understand why.
The only success I've had was when using EventCallbacks instead of the events. But I am using events in lots of other places and they all seem to work fine.
Could somebody tell me why events seem to fail and how this can be fixed?
Try this code
public async Task OnLoadStart()
{
this.IsLoading = true;
await InvokeAsync(() => { StateHasChanged(); });
}
public async Task OnLoadFinish()
{
this.IsLoading = false;
await InvokeAsync(() => { StateHasChanged(); });
}
protected override void OnInitialized()
{
this.ResultContainerStateManager.OnLoadStart += OnLoadStart;
this.ResultContainerStateManager.OnLoadFinish += OnLoadFinish;
}
Change : public event Action OnLoadStart;
To: public event Func<Task> OnLoadStart;
And: public event Action OnLoadFinish;
Tp: public event Func<Task> OnLoadFinish;
Implement IDisposable in the HeaderComponent component:
#implements IDisposable
public void Dispose()
{
this.ResultContainerStateManager.OnLoadStart -= OnLoadStart;
this.ResultContainerStateManager.OnLoadFinish -= OnLoadFinish;
}
Start coding asynchronously wherever you can.

How to open a page from App.xaml.cs if not open already

I have a Xaml.Forms app that uses FreshMVVM. I open a certain page from app.xaml.cs like this:
Xamarin.Forms.Device.BeginInvokeOnMainThread(async () =>
{
var navService = FreshIOC.Container.Resolve<IFreshNavigationService>(FreshMvvm.Constants.DefaultNavigationServiceName);
Page page = FreshPageModelResolver.ResolvePageModel<SomePageModel>();
await navService.PushPage(page, null);
...
});
But I need to add a check to prevent doing this if this page is already open. How can I make such a check?
Add a static bool value in the App class to check if the page has been opened:
public partial class App : Application
{
public static bool isPageOpened;
public App()
{
InitializeComponent();
MainPage = new MainPage();
}
public void test()
{
if (App.isPageOpened = false)
{
Xamarin.Forms.Device.BeginInvokeOnMainThread(async () =>
{
var navService = FreshIOC.Container.Resolve<IFreshNavigationService>(FreshMvvm.Constants.DefaultNavigationServiceName);
Page page = FreshPageModelResolver.ResolvePageModel<SomePageModel>();
App.isPageOpened = true;
await navService.PushPage(page, null);
});
}
}
}
And in the page's OnDisappearing method, set the isPageOpened to false:
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
protected override void OnDisappearing()
{
base.OnDisappearing();
App.isPageOpened = false;
}
}

Xamarin Forms:Prism:Android:MainActivity: Click on Push Notifications: PushAsync not supported globally on Android, please use a NavigationPage

I am trying to implement a basic push notification example using
Xamarin Forms with Prism MVVM, Azure & FCM.
I am receiving notification, but couldn't navigate to a specific page when clicked on the notification.
Trying basic functionality when the app is running or in the background (not closed).
It's throwing an exception "PushAsync not supported globally on Android, please use a NavigationPage" at
ExploreXam.App.Current.MainPage.Navigation.PushAsync(page);
[Activity(LaunchMode = LaunchMode.SingleTask, MainLauncher = true]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
internal static readonly string CHANNEL_ID = "explore_xamarin";
internal static readonly int NOTIFICATION_ID = 1029;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
CreateNotificationChannel();
global::Xamarin.Forms.Forms.Init(this, bundle);
LoadApplication(new App());
}
protected override void OnNewIntent(Intent intent)
{
base.OnNewIntent(intent);
Intent = intent;
NotificationClickedOn(intent);
}
private void NotificationClickedOn(Intent intent)
{
if (intent.Action == ExploreXamFirebaseMessagingService.ExploreXamNotification && intent.HasExtra("XamId"))
{
var page = new Xamarin.Forms.NavigationPage(new SpecificPage());
Xamarin.Forms.Application.Current.MainPage.Navigation.PushAsync(page);
ExploreXam.App.Current.MainPage.Navigation.PushAsync(page);
}
}
}
public partial class App : PrismApplication
{
public bool navigating;
public App(IPlatformInitializer initializer = null, bool shallNavigate=false) : base(initializer)
{
navigating = shallNavigate;
}
protected async override void OnInitialized()
{
BlobCache.ApplicationName = "ExploreXam";
InitializeComponent();
FlowListView.Init();
//await NavigationService.NavigateAsync("LoginPage");
await NavigationService.NavigateAsync("NavigationPage/LoginPage");
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
//mapping
}
}
Any idea that would help out, please?
You could access Prims's NavigationService instance to achieve what you're trying to do. But, it's a protected property. So, first you'd have to expose it through your App class as below :
public new INavigationService NavigationService => base.NavigationService;
Now, you can access the NavigationService from anywhere in your app by simply referencing it through your App as below:
(Xamarin.Forms.Application.Current as App).NavigationService.NavigateAsync("your/page/path");
So, your App class would look something like this:
public partial class App : PrismApplication
{
public new INavigationService NavigationService => base.NavigationService;
public bool navigating;
public App(IPlatformInitializer initializer = null, bool shallNavigate=false) : base(initializer)
{
navigating = shallNavigate;
}
protected async override void OnInitialized()
{
BlobCache.ApplicationName = "ExploreXam";
InitializeComponent();
FlowListView.Init();
//await NavigationService.NavigateAsync("LoginPage");
await NavigationService.NavigateAsync("NavigationPage/LoginPage");
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
//mapping
}
}
And your NotificationClickOn function would become something like :
private async void NotificationClickedOn(Intent intent)
{
if (intent.Action == ExploreXamFirebaseMessagingService.ExploreXamNotification && intent.HasExtra("XamId"))
{
var navigationService = (Xamarin.Forms.Application.Current as ContosoCookbook.App).NavigationService;
await navigationService.NavigateAsync("YourNavigationPage/SpecificPage");
}
}
The reason this is happening is because your Application.Current.MainPage is not a Navigation page but a ContentPage (i assume)
Wrap your initial MainPage in a NavigationPage as show below and it should work
In your App.xaml.cs
MainPage= new NavigationPage(new FirstPage());
I agree with #chaosifier. Create a public INavigationService in your App.xaml.cs file and then in the OnInitialized() method make the public property = the the base.NavigationService;
public INavigationService PrismNavigation { get; private set; }
protected override async void OnInitialized()
{
InitializeComponent();
PrismNavigation = base.NavigationService;
}
Then from the MainActivity.cs file you can navigate using something like this
(Xamarin.Forms.Application.Current as App).PrismNavigation.NavigateAsync(nameof(ShowAlertsDetailPage));
I hope this helps.

Resources