How to put an ActivityIndicator on Xamarin Forms OnStart() function.
I am check Network access on OnStart() function.
Bind the ActivityIndicator to a property in your BaseViewModel (IsBusy).
View
<ActivityIndicator Color="Accent" IsVisible="{Binding IsBusy}" IsRunning="{Binding IsBusy}" />
BaseViewModel (Inherited by all ViewModels)
private bool _isBusy;
public bool IsBusy
{
get { return _isBusy; }
set
{
_isBusy = value;
OnPropertyChanged("IsBusy");
}
}
Get yourself a good MVVM framework (Prism) and put the network check in the OnNavigatedTo method for your start page.
public override void OnNavigatedTo(INavigationParameters parameters)
{
IsBusy = true;
await CheckNetwork();
IsBusy = false;
}
Now you can paste that same ActivityIndicator snippet into any page (XAML) that is bound to a ViewModel inheriting BaseViewModel and it will just work when you set IsBusy.
Haven't used ActivityIndicator, but this nuget works great: Acr.UserDialogs.
After installing and adding the initialization part in the MainActivity or ios equivalent, just add the following code between resource intensive threads in either your code-behind file or viewmodel (mvvm):
This works for code-behind file:
protected override async void OnAppearing(object sender, EventArgs e)
{
base.ViewIsAppearing(sender, e);
UserDialogs.Instance.ShowLoading();
//do stuff here
UserDialogs.Instance.HideLoading();
}
This works for FreshMVVM framework:
protected override async void ViewIsAppearing(object sender, EventArgs e)
{
base.ViewIsAppearing(sender, e);
UserDialogs.Instance.ShowLoading();
//do stuff here
UserDialogs.Instance.HideLoading();
}
I'm using network checking in my projects too, please check this:
using Plugin.Connectivity;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace PetBellies.View
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class NoConnection : ContentPage
{
private bool wasNotConn = false;
public NoConnection()
{
InitializeComponent();
CrossConnectivity.Current.ConnectivityChanged += async (sender, args) =>
{
if (CrossConnectivity.Current.IsConnected && !wasNotConn)
{
wasNotConn = true;
await Navigation.PushModalAsync(new NavigationPage(new MainPage()));
}
else
{
wasNotConn = false;
}
};
}
public NoConnection(bool isFromLogin)
{
CrossConnectivity.Current.ConnectivityChanged += async (sender, args) =>
{
if (CrossConnectivity.Current.IsConnected && !wasNotConn)
{
wasNotConn = true;
var page = new LoginPage();
var navPage = new NavigationPage(page);
NavigationPage.SetHasNavigationBar(navPage, false);
await Navigation.PushModalAsync(navPage);
}
else
{
wasNotConn = false;
}
};
}
}
}
https://github.com/officialdoniald/PetBellies/blob/master/PetBellies/PetBellies/View/NoConnection.xaml.cs
If the connection lost, the application navigate to this page and stay on this page while the connection is unavailable.
Related
I have a class SplashScreen where I show an image scaling and after it finishes scaling it goes to a login page. I tried this on multiple devices and only in one device it doesn't change from the SplashScreen.
I've been researching how to handle async methods and await instructions but nothing seems to work so far. Also I tried removing the ScaleTo and just show the image but it doesn't work.
This is what I have:
protected override async void OnAppearing()
{
base.OnAppearing();
await splashLogo.ScaleTo(1.5, 3000);
ShowLogin();
}
I solved this problem by adding this class:
class MainPageViewModel : BaseViewModel
{
private bool isLoadingData;
public bool IsLoadingData
{
get => isLoadingData;
set => SetProperty(ref isLoadingData, value);
}
public async Task LoadData()
{
IsLoadingData = true;
await Task.Delay(2000);
IsLoadingData = false;
}
}
Then in my SplashScreen page:
public SplashScreen()
{
//Everything else I need in this page
BindingContext = new MainPageViewModel();
}
private MainPageViewModel ViewModel => BindingContext as MainPageViewModel;
protected override async void OnAppearing()
{
base.OnAppearing();
await ViewModel.LoadData();
ShowLogin();
}
I'm using Xamarin Forms and I'm having an issue use InsertPageBefore() method with existing objects of Pages.
Here is my view code:
private FirstPage firstPage;
private SecondPage secondPage = new SecondPage();
private ThirdPage thirdPage = new ThirdPage();
private async void ItemSelectedMethod()
{
var root = App.NavigationPage.Navigation.NavigationStack[0];
if (SelectedItem == Items[0])
{
if (!IsFirstChoose)
{
App.NavigationPage.Navigation.InsertPageBefore(firstPage, root);
await App.NavigationPage.PopToRootAsync(false);
}
}
if (SelectedItem == Items[1])
{
App.NavigationPage.Navigation.InsertPageBefore(secondPage, root);
await App.NavigationPage.PopToRootAsync(false);
}
if (SelectedItem == Items[2])
{
App.NavigationPage.Navigation.InsertPageBefore(thirdPage, root);
await App.NavigationPage.PopToRootAsync(false);
}
IsFirstChoose = false;
rootPageViewModel.IsPresented = false;
}
It's throw exception "System.ArgumentException: 'Cannot insert page which is already in the navigation stack'". How to switch between existing objects of pages? I don't want create new object in InsertPageBefore(). I tried use it code, before call InsertPageBefore():
foreach (var item in App.NavigationPage.Navigation.NavigationStack.ToList())
App.NavigationPage.Navigation.RemovePage(item);
But it's not working... Can anyone help me?
It didn't work with UWP. Here is agly workaround for you but you really need to read how to work with Master-Detail pages.
public partial class App : Application
{
public static RootPage RootPage { get; private set; } //DON'T DO THIS,
//FIND A BETTER WAY
public App()
{
InitializeComponent();
RootPage = new RootPage();
MenuPage menuPage = new MenuPage(RootPage.vm);
RootPage.Master = menuPage;
RootPage.Detail = new NavigationPage(new MainPage());// NavigationPage;
MainPage = RootPage;
}
protected override void OnStart()
{
// Handle when your app starts
}
protected override void OnSleep()
{
// Handle when your app sleeps
}
protected override void OnResume()
{
// Handle when your app resumes
}
}
Then
private async void ItemSelectedMethod()
{
if (SelectedItem == Items[0])
{
App.RootPage.Detail = new NavigationPage(mainPage);
}
if (SelectedItem == Items[1])
{
App.RootPage.Detail = new NavigationPage(secondPage);
}
if (SelectedItem == Items[2])
{
App.RootPage.Detail = new NavigationPage(thirdPage);
}
rootPageViewModel.IsPresented = false;
}
I can't seem to get a method in my ViewModel to run successfully from my XAML code behind when using NotifyIcon. The method executes correctly, as tested with debugging mode using breakpoints, but nothing happens in the View.
The method in question is RefreshData, and it can be called from either a button in the View (works as expected), or from right clicking the NotifyIcon and selecting Refresh Data (does nothing). I'll post relevant code below. Any help would be appreciated!
MainWindow constructor in CodeBehind
public MainWindow()
{
try
{
MM = new MMViewModel();
InitializeComponent();
DataContext = MM;
_notifyIcon = new NotifyIcon();
_notifyIcon.DoubleClick += (s, args) => ShowMainWindow(this);
_notifyIcon.Icon = Migration_Monitor_v2.Properties.Resources.mmc;
_notifyIcon.Visible = true;
Closing += MainWindow_Closing;
CreateContextMenu();
}
catch (Exception e)
{
logger.Error("App failed with exception: ", e);
}
}
private void CreateContextMenu()
{
_notifyIcon.ContextMenuStrip = new ContextMenuStrip();
_notifyIcon.ContextMenuStrip.Items.Add("Refresh Data").Click += (s,e) => MM.RefreshData();
_notifyIcon.ContextMenuStrip.Items.Add("Exit").Click += (s, e) => ExitApplication(this);
}
RefreshData method in ViewModel (works when executed from the Refresh button in the View)
public void RefreshData()
{
InfoPanelVisible = Visibility.Hidden;
InfoSummaryVisible = Visibility.Visible;
Task.Run(() => LoadData());
n = DateTime.Now;
ProgressBarText = "Click a project to show progress";
ProgressBarValue = 0;
lastRefresh.Reset();
lastRefresh.Start();
}
LoadData method (and associated methods) called from RefreshData
public async void LoadData()
{
IsLoading = Visibility.Visible;
await GetWebApiInfo();
MonitorData downloadInfo = main;
try { AssignDataToControls(downloadInfo); }
catch (Exception e) { Console.WriteLine("Error: " + e); }
finally { IsLoading = Visibility.Hidden; }
}
public void AssignDataToControls(MonitorData mon)
{
MainPanel.Clear();
MonitorText.Clear();
mon.MainPanel.ToList().ForEach(x => MainPanel.Add(x));
mon.MonitorText.ToList().ForEach(x => MonitorText.Add(x));
Information = mon.Information;
ProgressData = mon.progList;
}
public async Task GetWebApiInfo()
{
var url = "::::WEB API ADDRESS::::";
string responseFromServer;
using (HttpClient _client = new HttpClient())
using (var dataStream = await _client.GetStreamAsync(url))
using (var reader = new StreamReader(dataStream, Encoding.Unicode))
responseFromServer = await reader.ReadToEndAsync();
var deserializer = new JavaScriptSerializer();
main = deserializer.Deserialize<MonitorData>(responseFromServer);
}
RefreshCommand from ViewModel Commands.cs file
internal class RefreshCommand : ICommand
{
public RefreshCommand(MMViewModel viewModel)
{
_viewModel = viewModel;
}
private MMViewModel _viewModel;
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public bool CanExecute(object parameter)
{
return _viewModel.CanRefresh;
}
public void Execute(object parameter)
{
_viewModel.RefreshData();
}
}
Solved this issue. The problem was that my ViewModel was being constructed three separate times, so it was unclear to the command which instance was being referenced.
To fix this, I removed references to the ViewModel in my XAML Window definition space. There were 2 references I thought I needed for the ViewModel as a local namespace (i.e. x:local.VM="MM.MyViewModel"). After removing those, the ViewModel is only being constructed once and all code is running as intended. Thanks to #Will for his help getting to the bottom of this!
I have registered a handler with the HardwareButtons.BackPressed event, performed some logic and then set the handled property in the args to true if it applies. The handler runs through without any issue, and the Handled property gets set. The phone still navigates back outside of the app. Am I misunderstanding how to use the event?
Page
public sealed partial class FirstRunPage : VisualStateAwarePage
{
public FirstRunPage()
{
this.InitializeComponent();
#if WINDOWS_PHONE_APP
HardwareButtons.BackPressed += (sender, args) =>
{
bool isHandled = false;
Action handledCallback = () => isHandled = true;
var state = new Dictionary<string, object> { { "Callback", handledCallback } };
((INavigationAware)this.DataContext).OnNavigatedTo("Back", NavigationMode.Back, state);
args.Handled = isHandled;
};
#endif
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
}
}
View model.
public override void OnNavigatedTo(object navigationParameter, NavigationMode navigationMode, Dictionary<string, object> viewModelState)
{
if (navigationParameter == null || !navigationParameter.ToString().Equals("Back"))
{
return;
}
if (!viewModelState.ContainsKey("Callback"))
{
return;
}
var callback = (Action)viewModelState["Callback"];
// If the user is new, then we set it to false and invoke our callback.
if (this.IsNewUser)
{
this.IsNewUser = false;
callback();
}
else
{
return;
}
}
Update
I have modified my FirstRunPage to subscribe and unsubscribe as recommended by #Martin but it still closes the app.
public sealed partial class FirstRunPage : VisualStateAwarePage
{
public FirstRunPage()
{
this.InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
#if WINDOWS_PHONE_APP
HardwareButtons.BackPressed += HardwareBack_OnPressed;
#endif
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
#if WINDOWS_PHONE_APP
HardwareButtons.BackPressed -= HardwareBack_OnPressed;
#endif
}
private void HardwareBack_OnPressed(object sender, BackPressedEventArgs e)
{
Action handledCallback = () => e.Handled = true;
var state = new Dictionary<string, object> { { "Callback", handledCallback } };
((INavigationAware)this.DataContext).OnNavigatedTo("Back", NavigationMode.Back, state);
}
}
With the help of #yasen I was able to get this resolved. The issue stems from the fact that the Prism library has your App.xaml.cs inherit from MvvmAppbase, which intercepts the BackPressed event.
To resolve this, I overrode the MvvmAppBase OnHardwareButtonsBackPressed and added a bit of logic to handle it.
My view model and view both implement a new interface called INavigateBackwards and they're used like this:
View Model
public bool CanNavigateBack()
{
// If the new user is true, then we can't navigate backwards.
// There isn't any navigation stack, so the app will die.
bool canNavigate = !this.IsNewUser;
// Disable the new user mode.
this.IsNewUser = false;
// Return so that the view can return to it's sign-in state.
return canNavigate;
}
View
public sealed partial class FirstRunPage : VisualStateAwarePage, INavigateBackwards
{
private INavigateBackwards ViewModel
{
get
{
return (INavigateBackwards)this.DataContext;
}
}
public FirstRunPage()
{
this.InitializeComponent();
}
public bool CanNavigateBack()
{
return ViewModel.CanNavigateBack();
}
}
Then in the MvvmAppBase subclass, I determine if I need to handle the navigation or not.
MvvmAppBase child
protected override void OnHardwareButtonsBackPressed(object sender, BackPressedEventArgs e)
{
var page = (Page)((Frame)Window.Current.Content).Content;
if (page is INavigateBackwards)
{
var navigatingPage = (INavigateBackwards)page;
if (!navigatingPage.CanNavigateBack())
{
e.Handled = true;
return;
}
}
base.OnHardwareButtonsBackPressed(sender, e);
}
This allows my single view to have multiple states and the user to navigate back from one state to the previous without navigating to an entirely new view.
The reason why your application closes is that the same handler is called more than just once. First handler sets the Handled property to true, but any other subsequent call for the same event fire sets it back to false.
To illustrate it, try this:
public sealed partial class FirstRunPage : VisualStateAwarePage
{
public FirstRunPage()
{
// ...
InitializeComponent();
HardwareButtons.BackPressed += HardwareButtons_BackPressed;
HardwareButtons.BackPressed += HardwareButtons_BackPressed;
}
void HardwareButtons_BackPressed(object sender, BackPressedEventArgs e)
{
bool isHandled = false;
Action handledCallback = () => isHandled = true;
var state = new Dictionary<string, object> { { "Callback", handledCallback } };
((INavigationAware)this.DataContext).OnNavigatedTo("Back", NavigationMode.Back, state);
args.Handled = isHandled;
};
}
And set breakpoint to last line of the handler code.
To avoid it, assign your handler in the OnNavigatedTo method of your FirstRunPage, and unregister the handler in OnNavigatedFrom.
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
HardwareButtons.BackPressed += HardwareButtons_BackPressed;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
HardwareButtons.BackPressed -= HardwareButtons_BackPressed;
}
I am trying to implement the busy indicator from the WPF Extended Toolkit in my application's "shell." The goal is to implement the indicator in one place and then be able to set the IsBusy property from anywhere so that it can be initialized. Here is my shell:
<Window x:Class="Foundation.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Library.Controls.Views;assembly=Library"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
WindowStyle="None"
AllowsTransparency="False"
Name="ShellView"
FontFamily="Yu Gothic Light"
Background="{StaticResource AiWhiteBrush}">
<!--Region Outer Most Grid-->
<xctk:BusyIndicator IsBusy="{Binding IsBusy}">
<Grid x:Name="OuterGrid">
<!-- CONTENT HERE -->
</Grid>
</xctk:BusyIndicator>
<!--End Region-->
Then, my Shell's ViewModel looks like this:
using CashDrawer.Views;
using Library.BaseClasses;
using Library.StaticClasses;
using Microsoft.Practices.Prism.Commands;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using WpfPageTransitions;
namespace Foundation
{
public class ShellViewModel : ViewModelBase
{
#region constructor(s)
public ShellViewModel()
{
StateManager.IsBusyChange += new StateManager.IsBusyHandler(IsBusyEventAction);
}
#endregion constructor(s)
#region properties
private bool _IsBusy;
public bool IsBusy
{
get
{
return _IsBusy;
}
set
{
if (_IsBusy != value)
{
_IsBusy = value;
OnPropertyChanged("IsBusy");
}
}
}
#endregion properties
#region actions, functions, and methods
private void IsBusyEventAction(object sender, EventArgs e)
{
if (StateManager.IsBusy)
{
this.IsBusy = true;
}
else
{
this.IsBusy = false;
}
}
#endregion actions, functions, and methods
}
}
Last, I have created a static StateManager class:
using System;
using System.Collections.ObjectModel;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using WpfPageTransitions;
namespace Library.StaticClasses
{
public static class StateManager
{
private static bool _IsBusy;
public static bool IsBusy
{
get
{
return _IsBusy;
}
set
{
if (_IsBusy != value)
{
_IsBusy = value;
IsBusyChange(null, null);
}
}
}
public delegate void IsBusyHandler(object sender, EventArgs e);
public static event IsBusyHandler IsBusyChange;
}
}
The idea is that when the StateManager's IsBusy property is changed, it will fire an event that will change the IsBusy property in the ShellViewModel accordingly. The logic is working fine. However, the busy indicator isn't working as expected. Here is a code snippet from another view model that switches the IsBusy property:
private void SaveCommand_Action()
{
StateManager.IsBusy = true;
this.Validate();
if (!HasValidationErrors)
{
if (this.CustomerControlVM.SaveCustomer() != 0)
{
VehicleControlVM.VehicleModel.CustomerID = this.CustomerControlVM.CustomerModel.CustomerID;
this.VehicleControlVM.SaveVehicle();
ComplaintsView complaintsControl = new ComplaintsView();
(complaintsControl.DataContext as ComplaintsViewModel).CurrentVehicle = this.VehicleControlVM.VehicleModel;
(complaintsControl.DataContext as ComplaintsViewModel).CurrentCustomer = this.CustomerControlVM.CustomerModel;
StateManager.LoadView(complaintsControl, PageTransitionType.SlideLeft);
}
}
StateManager.IsBusy = false;
}
I am seeing some lag in the code, but I never see the busy indicator appear. I can remove StateManager.IsBusy = false; and the busy indicator will appear (and show indefinitely of course.) I have tried creating a longer delay between the IsBusy state changes and the indicator still doesn't appear. I have read multiple posts and articles trying to understand what may be going wrong but I am not seeing anything helpful. I am aware that the IsBusy indicator is happening on the UI thread, but I am changing the IsBusy states in the ViewModel which should not be on the UI thread. Any thoughts or suggestions?
Regarding my last comment.
You could change the statemanager to take in an action instead, so you pass in the method, and the state manager can setup the invoke etc
public static class StateManager
{
public static void Process(Action action) {
IsBusy = true;
Application.Current.Dispatcher.Invoke(action, System.Windows.Threading.DispatcherPriority.Background);
IsBusy = false;
}
private static bool _IsBusy;
public static bool IsBusy {
get {
return _IsBusy;
}
set {
if (_IsBusy != value) {
_IsBusy = value;
IsBusyChange(null, null);
}
}
}
public delegate void IsBusyHandler(object sender, EventArgs e);
public static event IsBusyHandler IsBusyChange;
}
Then you could do:
private void SaveCommand_Action()
{
StateManager.Process(() =>
{
this.Validate();
if (!HasValidationErrors)
{
if (this.CustomerControlVM.SaveCustomer() != 0)
{
VehicleControlVM.VehicleModel.CustomerID = this.CustomerControlVM.CustomerModel.CustomerID;
this.VehicleControlVM.SaveVehicle();
ComplaintsView complaintsControl = new ComplaintsView();
(complaintsControl.DataContext as ComplaintsViewModel).CurrentVehicle = this.VehicleControlVM.VehicleModel;
(complaintsControl.DataContext as ComplaintsViewModel).CurrentCustomer = this.CustomerControlVM.CustomerModel;
StateManager.LoadView(complaintsControl, PageTransitionType.SlideLeft);
}
}
StateManager.IsBusy = false;
});
}
Thanks to sa_ddam213, I have the issue under wraps. The problem was the priority. This code is what took care of it:
private void SaveCommand_Action()
{
StateManager.IsBusy = true;
Application.Current.Dispatcher.Invoke(() => Save(), System.Windows.Threading.DispatcherPriority.Background);
}
private void Save()
{
this.Validate();
if (!HasValidationErrors)
{
if (this.CustomerControlVM.SaveCustomer() != 0)
{
VehicleControlVM.VehicleModel.CustomerID = this.CustomerControlVM.CustomerModel.CustomerID;
this.VehicleControlVM.SaveVehicle();
ComplaintsView complaintsControl = new ComplaintsView();
(complaintsControl.DataContext as ComplaintsViewModel).CurrentVehicle = this.VehicleControlVM.VehicleModel;
(complaintsControl.DataContext as ComplaintsViewModel).CurrentCustomer = this.CustomerControlVM.CustomerModel;
StateManager.LoadView(complaintsControl, PageTransitionType.SlideLeft);
StateManager.IsBusy = false;
}
}
}
I have a little more work to do so I don't need to do this with each IsBusy state change, but with what I've learned, I can figure it out. Thanks so much sa_ddam213.