testing navigation from one view model to other viewmodel - xamarin

I am trying to create test which be able to check navigation between two viewmodels.
Some code from ResultViewModel
private IMvxCommand homeCommand;
public IMvxCommand HomeCommand => homeCommand ?? (homeCommand = new MvxAsyncCommand(ShowHomePage));
private async Task ShowHomePage()
{
await _navigationService.Navigate<HomeViewModel>();
}
Testing code
private Mock mvxNavigationService = new Mock();
private Mock resultService = new Mock();
[Test]
public void Test1()
{
var viewModel = new ResultViewModel(mvxNavigationService.Object, resultService.Object);
viewModel.HomeCommand.Execute();
mvxNavigationService.Verify(service => service.Navigate<HomeViewModel>());
Assert.Pass();
}
I am getting error
"An expression tree cannot contain a call or invocation that uses optional arguments."
on line
mvxNavigationService.Verify(service => service.Navigate());
Not sure where the issue is.

In Xamarin, a dedicated page navigation method is officially provided.
You can navigate between pages like:
await Navigation.PushAsync (new Page());
You can pop pages from the navigation stack like:
await Navigation.PopAsync ();
For more usage, please refer to: Hierarchical Navigation.

Related

State of RefreshView is invoking RefreshCommand in .NET MAUI

I have a CollectionView in my .NET MAUI app and I placed it inside a RefreshView. When I call my API to populate this CollectionView, I cache the data so that I don't have to keep making API calls everytime the user hits this page.
In order to streamline my code, I created a private method in my view model that calls my API. The view model looks like this:
public partial MyViewModel : BaseViewModel
{
ObservableCollection<MyModel> MyData { get; } = new();
[RelayCommand]
async Task RefreshData()
{
IsBusy = true;
await GetData(true);
IsBusy = false;
}
private async Task GetData(bool shouldGetFreshData)
{
// Makes API call to get data, then assigns it to MyData collection
}
public async void Init()
{
IsBusy = true;
await GetData(false);
IsBusy = false;
}
}
The XAML for the page looks like this:
<RefreshView
IsRefreshing={Binding IsBusy}
Command={Binding RefreshDataCommand}>
<CollectionView>
...
</CollectionView>
</RefreshView>
I also wired the page to use the MyViewModel as its view model AND OnAppearing(), I call the Init() method of the view model.
Here's what I was expecting which is NOT what's happening:
I thought, the Init() would get called first which then calls the GetData() method with false input parameter. This way, I could use the cached data. And whenever, the user refreshes the CollectionView by pulling it down, the RefreshData() method would be called with true as the input parameter which would force the code to make an API call.
Instead of what I was expecting, here's what's happening:
The Init() method gets called first and as a result, the line with IsBusy = true executes.
This then ends up invoking the RefreshData() method
Then the await GetData(false) in Init() method executes
Then the await GetData(true) in RefreshData() method executes
As a result of all this, the GetData() method gets called twice.
I think, what's triggering this is the IsBusy. I thought IsBusy would only serve as an indicator but not necessarily invoke the RefreshData() method which is bound to the Command of my RefreshView.
Is this normal behavior or am I missing something here?
Apparently, this is "normal" behavior because I'm manually setting IsBusy to true. I decided to leave this question here because this may be a pitfall that affects others.
Here's the actual section in documentation that states this:
And here's the documentation: https://learn.microsoft.com/en-us/dotnet/maui/user-interface/controls/refreshview
So, all I had to do is remove the IsBusy = true in Init() method.

Mock and test MvvmCross NavigationService

I am using the navigation service to perform the navigation between ViewModels and now I have to create some unit tests for my ViewModel. I am able to mock the ViewModel creating the injected objects with Moq but I am struggling to mock NavigationService and then create the assertion than it can navigate to the next ViewModel.
I have found some documentation from a couple of years ago so the navigation service did not exist and I cannot find anything about this matter at official MvvmCross documentation so right now I am in a dead end.
I am using MvvmCross 5.5.0.
[Test]
public void CheckStatus()
{
Mock<IStatusService> _mockStatusService = new Mock<IStatusService>();
_mockStatusService.Setup(x => x.Check(It.IsAny<StatusRequest>())).Returns(() => new StatusRequest { Ok = true });
//Here I need to pass the NavigationService to the constructor
var _viewModel = new StatusViewModel(_mockStatusService.Object, navigationService);
_viewModel.Start();
//Here I guess I should perform the navigation assertion.
}
I have found how to mock and test the NavigationService. I post here the solution because it would be useful for someone else.
[Test]
public void CheckStatus()
{
IFixture _fixture = new Fixture().Customize(new AutoMoqCustomization());
Mock<IMvxNavigationService> _navigationService = _fixture.Freeze<Mock<IMvxNavigationService>>();
var _viewModel = new StatusViewModel(navigationService);
_viewModel.Start();
_navigationService.Verify(x => x.Navigate<NextViewModel>(null), Times.Once());
}

Button is disabled when binding

I have a IMvxAsyncCommand method in my ViewModel to perform a server call and depending the result navigate to a ViewModel or other. I don't know why but it is disabling the button where the click event is bind.
public IMvxAsyncCommand Register
{
var runner = Task.Run(() => _service.Register());
runner.Wait();
if (runner.Result.Status == SUCCESS)
{
return new MvxAsyncCommand(() => _navigationService.Navigate<NextViewModel>());
}
else
{
return new MvxAsyncCommand(() => _navigationService.Navigate<ErrorViewModel>());
}
}
I tried moving the runner into an async method and set a global variable with the response but I continue to experience this issue. If I comment the service call and leave just the navigationService code then the button is enabled so I guess the problem is the way I am making the service call into this method.
First of all your code snippet does not compile. If it were a Property Getter, it would block until _service.Register() completes, which is probably not what you want. Instead you should put that call into the command and await it:
private IMvxAsyncCommand _register;
public ICommand Register => _register = _register ?? new MvxAsyncCommand(DoRegisterCommand);
private async Task DoRegisterCommand()
{
var result = await _service.Register();
if (result.Status == SUCCESS)
await _navigationService.Navigate<NextViewModel>();
else
await _navigationService.Navigate<ErrorViewModel>();
}

Xamarin form MessagingCenter Unsubscribe is not working as expected

Functionality written inside the MessagingCenter.Subscribe() is called multiple times when i navigate to and fro multiple times in the application. But each time before subscribing, i do unsubscribe to the same in constructor as follows, still it didn't worked.
MessagingCenter.Unsubscribe<SubmitPage>(this,"Save");
MessagingCenter.Subscribe<SubmitPage>(this, "Save", (sender) =>
{
DisplayToastOnSuccessfulSubmission();
});
In my application i have 6 pages(git) and i save the data in 6th page with MessagingCenter.Send and same will be subscribed in 2nd page and saved message will be displayed in 2nd page(after navigating to that page).
Now i navigate like 2->1->2->3->4->5->6 in this particular case DisplayToastOnSuccessfulSubmission() would be called two times(because Page2 constructor is called twice).
I even tried placing the same code in OnAppearing.
I can't unsubscribe in OnDisappear as I need the event wiring up to when I reach Page6 for save.
Reproduced the same behaviour in sample project and added here https://github.com/suchithm/MessageCenterSampleApp Drop box link
What is the proper way to do this?
But each time before subscribing, I do unsubscribe to the same in constructor as follows, still it didn't worked.
MessagingCenter.Subscribe() is called multiple times, because there are two instances of Page2 in your code, both of them use MessagingCenter.Subscribe() method, that's why the Unsubscribe didn't work.
You can modify page2() to a singleton to make sure there is only one instance of Page2 in your project, after that when you send a message,
the MessagingCenter.Subscribe() is called only once.
Page2.cs:
public static Page2 instance = new Page2();
public static Page2 GetPage2Instance()
{
if(instance == null)
{
return new Page2();
}
return instance;
}
private Page2()
{
InitializeComponent();
MessagingCenter.Unsubscribe<Page2>(this, "SaveToastPage2");
MessagingCenter.Subscribe<Page2>(this, "SaveToastPage2", (sender) =>
{
DisplayToastOnSuccessfulSubmission();
}
}
When you send a message :
MessagingCenter.Send(Page2.GetPage2Instance(), "SaveToastPage2");
EDIT :
Remember that declaring constructors of Page2 class to be private to make sure there is only one instance of Page2 in your project sure.
private Page2()
{
...
}
Modify your Page1.cs code :
async void Handle_Next(object sender, System.EventArgs e)
{
await App.NavigationRef.PushAsync(Page2.GetPage2Instance(), true);
}
I faced same issue. I solved issue by passing the same parameters inn subscribe and unsubscribing as well.
MessagingCenter.Subscribe<Page1, T>(this, "Listen", async (Page1 arg1, T
listenedString) =>
{
});
Unsubscribe like below
MessagingCenter.Unsubscribe<Page1, T>(this, "Listen");
I'm using this temporary solution.
I declared a static dictionary to storage my object (to this example I used an object type).
private static Dictionary<string, object> subscribedReferencePages = new Dictionary<string, object>();
And I always storage the last subscribed page reference.
Then I compare the page reference before triggering the message method to fire only the last one.
subscribedReferencePages[pageName] = this;
MessagingCenter.Subscribe<ViewModelBase>(this, pageName, async (sender) =>
{
if (!ReferenceEquals(sender, this))
{
return;
}
this.OnInitialized();
});
To call the message method I need to pass the dictionary as parameter (instead of the "this" reference).
MessagingCenter.Send(subscribedPages[pageName], keyPageName);
Instead of unsubscribing when you navigate TO a page,
unsubscribe when you navigate AWAY from the page. At that point your instance of 'this' is still the same 'this' you think it is.

Xamarin Forms Navigation without animation

I have an app where I want to show page A, from which the user can navigate to page B or C, from B back to A or to C, and from C only back to A, even if the user when through B to get to C
Currently when I'm executing the B->C transition I first PopAsync to get back to A and then I do PushAsync to get to C, so that the '
The question is: is there a civilized way to set up this navigation scheme while still relying on the built-in Navigation to keep track of navigation stack - I don't want to do that myself and use PushModalAsync.
Note that (as reflected in the image) A and C aren't the end points of the whole navigation stack, there are pages before A and after C, so the stack has to be preserved.
On iOS the NavigationRenderer has virtual methods OnPopViewAsync and OnPushAsync (similar on Android):
protected override Task<bool> OnPopViewAsync(Page page, bool animated)
{
return base.OnPopViewAsync(page, animated);
}
protected override Task<bool> OnPushAsync(Page page, bool animated)
{
return base.OnPushAsync(page, animated);
}
They call the corresponding base method with two arguments, the page and whether to animate the transition. Thus, you might be able to enable or disable the animation using the following approach:
Derive a custom navigation page.
Add an "Animated" property.
Derive a custom navigation renderer for your custom navigation page.
Override the pop and push methods calling their base methods with the "Animated" property.
Note that I haven't tried this approach, yet, since it is quite some work to do. But disabling animations on all navigation pages did work this way.
Edit: It took me several hours to actually implement my solution for my own project. Therefore, I'll share some more details. (I developed and tested on Xamarin.Forms 1.2.3-pre4.)
The custom navigation page
Besides the above-mentioned Animated property my navigation page re-implements the two transition functions and adds an optional argument animated, which is true by default. This way we'll be able to keep all existing code and only add a false where needed.
Furthermore, both method will sleep for a very short time (10 ms) after pushing/popping the page. Without this delay we'd ran into trouble with consecutive calls.
public class CustomNavigationPage: NavigationPage
{
public bool Animated { get; private set; }
public CustomNavigationPage(Page page) : base(page)
{
}
// Analysis disable once MethodOverloadWithOptionalParameter
public async Task PushAsync(Page page, bool animated = true)
{
Animated = animated;
await base.PushAsync(page);
await Task.Run(delegate {
Thread.Sleep(10);
});
}
// Analysis disable once MethodOverloadWithOptionalParameter
public async Task<Page> PopAsync(bool animated = true)
{
Animated = animated;
var task = await base.PopAsync();
await Task.Run(delegate {
Thread.Sleep(10);
});
return task;
}
}
The custom navigation renderer
The renderer for my custom navigation page overrides both transition methods and passes the Animated property to their base methods. (It's kind of ugly to inject a flag this way, but I couldn't find a better solution.)
public class CustomNavigationRenderer: NavigationRenderer
{
protected override Task<bool> OnPopViewAsync(Page page, bool animated)
{
return base.OnPopViewAsync(page, (Element as CustomNavigationPage).Animated);
}
protected override Task<bool> OnPushAsync(Page page, bool animated)
{
return base.OnPushAsync(page, (Element as CustomNavigationPage).Animated);
}
}
This is for iOS. But on Android it's almost identically.
An example application
To demonstrate the possibilities of consecutively pushing and popping pages, I wrote the following application.
The App class simply creates a new DemoPage wrapped into a CustomNavigationPage. Note that this instance must be publicly accessible for this example.
public static class App
{
public static CustomNavigationPage NavigationPage;
public static Page GetMainPage()
{
return NavigationPage = new CustomNavigationPage(new DemoPage("Root"));
}
}
The demo page contains a number of buttons that push and pop pages in different orders. You can add or remove the false option for each call to PushAsync or PopAsync.
public class DemoPage: ContentPage
{
public DemoPage(string title)
{
Title = title;
Content = new StackLayout {
Children = {
new Button {
Text = "Push",
Command = new Command(o => App.NavigationPage.PushAsync(new DemoPage("Pushed"))),
},
new Button {
Text = "Pop",
Command = new Command(o => App.NavigationPage.PopAsync()),
},
new Button {
Text = "Push + Pop",
Command = new Command(async o => {
await App.NavigationPage.PushAsync(new DemoPage("Pushed (will pop immediately)"));
await App.NavigationPage.PopAsync();
}),
},
new Button {
Text = "Pop + Push",
Command = new Command(async o => {
await App.NavigationPage.PopAsync(false);
await App.NavigationPage.PushAsync(new DemoPage("Popped and pushed immediately"));
}),
},
new Button {
Text = "Push twice",
Command = new Command(async o => {
await App.NavigationPage.PushAsync(new DemoPage("Pushed (1/2)"), false);
await App.NavigationPage.PushAsync(new DemoPage("Pushed (2/2)"));
}),
},
new Button {
Text = "Pop twice",
Command = new Command(async o => {
await App.NavigationPage.PopAsync(false);
await App.NavigationPage.PopAsync();
}),
},
},
};
}
}
Important hint: It cost me hours of debugging to find out that you need to use an instance of NavigationPage (or a derivative) rather than the ContentPage's Navigation! Otherwise the immediate call of two or more pops or pushes leads to strange behaviour and crashes.
#Falko
You now have the possibility to include a boolean parameter:
Navigation.PushAsync (new Page2Xaml (), false);
Xamarin Documentation
Currently the Xamarin Forms navigation is very spartanic and I doubt there is a nice way to achieve that. Besides Doing and extra "Pop" when necessary.
Here's a collection of snippets I whipped together along with some other niceties to improve NaviagationPage for iOS. Link to comment and code on xamarin forums.
What I would do if I were doing this is push Page C on to your NavigationStack and then take page B off of the stack. That way when you pop from page C, you would go to page A.
// Push the page you want to go to on top of the stack.
await NavigationPage.PushAsync(new CPage()));
// Remove page B from the stack, so when you want to go back next time
//you will go to page A.
Navigation.RemovePage(Navigation.NavigationStack[Navigation.NavigationStack.Count - 2] );
Alternatively, when even you pop from page C, you could remove all instances of type page B from the stack, and then pop back 1. In that case, page B would remain on the stack until you were about to move back from page C to page A.

Resources