I have a list of items that goes to another page, That page is hooked up to a view model. In the constructor of this view model I have code that grabs data from the server for that particular item.
What I found is that when I hit the back button and choose another item fromt hat list and it goes to the other page the constructor does not get hit.
I think it is because the VM is now created and thinks it does not need a new one. I am wondering how do I force a cleanup so that a fresh one is always grabbed when I select from my list?
I faced the same issue, that's how i solved it.
Have a BaseView class, override OnNavigatedTo
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (NavigatedToCommand != null && NavigatedToCommand.CanExecute(null))
NavigatedToCommand.Execute(null);
}
add DependencyProperty.
public static readonly DependencyProperty NavigatedToCommandProperty =
DependencyProperty.Register("NavigatedToCommand", typeof(ICommand), typeof(BaseView), null);
public ICommand NavigatedToCommand
{
get { return (ICommand)GetValue(NavigatedToCommandProperty); }
set { SetValue(NavigatedToCommandProperty, value); }
}
On the necessary pages, add to xaml (and, of course, inherit BaseView )
NavigatedToCommand="{Binding OnNavigatedToCommand}"
In the ViewModel, make command itself
public RelayCommand OnNavigatedToCommand
{ get { return new RelayCommand(OnNavigatedTo); } }
and implement method you want to call to update list
public async void OnNavigatedTo()
{
var result = await myDataService.UpdateMyList();
if (result.Status == OK)
MyList = result.List;
}
So, now, every time you navigate to page with list, inside of overriden OnNavigatedTo(), a NavigatedToCommand would be executed, which would execute OnNavigatedToCommand (which you set in xaml), which would call OnNavigatedTo, which would update your list.
A bit messy, but MVVM :)
EDIT: What about cleanings, they can be done in OnNavigatedFrom(), which works the same. Or OnNavigatingFrom(), which also can be useful in some cases.
Related
I've started working with the new navigation component and I'm really digging it! I do have one issue though - How am I supposed to handle the back button when I'm at the starting destination of the graph?
This is the code I'm using now:
findNavController(this, R.id.my_nav_host_fragment)
.navigateUp()
When I'm anywhere on my graph, it's working great, it send me back, but when I'm at the start of it - the app crashes since the backstack is empty.
This all makes sense to me, I'm just not sure how to handle it.
While I can check if the current fragment's ID is the same as the one that I know to be the root of the graph, I'm looking for a more elegant solution like some bool flag of wether or not the current location in the graph is the starting location or not.
Ideas?
I had a similar scenario where I wanted to finish the activity when I was at the start destination and do a regular 'navigateUp' when I was further down the navigation graph. I solved this through a simple extension function:
fun NavController.navigateUpOrFinish(activity: AppCompatActivity): Boolean {
return if (navigateUp()) {
true
} else {
activity.finish()
true
}
}
And then call it like:
override fun onSupportNavigateUp() =
findNavController(R.id.nav_fragment).navigateUpOrFinish(this)
However I was unable to use NavigationUI as this would hide the back arrow whenever I was at the start destination. So instead of:
NavigationUI.setupActionBarWithNavController(this, controller)
I manually controlled the home icon:
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_navigation_back)
Override onBackPressed in your activity and check if the current destination is the start destination or not.
Practically it looks like this:
#Override
public void onBackPressed() {
if (Navigation.findNavController(this,R.id.nav_host_fragment)
.getCurrentDestination().getId() == R.id.your_start_destination) {
// handle back button the way you want here
return;
}
super.onBackPressed();
}
You shouldn't override "onBackPressed", you should override "onSupportNavigateUp" and put there
findNavController(this, R.id.my_nav_host_fragment)
.navigateUp()
From the official documentation:
You will also overwrite AppCompatActivity.onSupportNavigateUp() and call NavController.navigateUp
https://developer.android.com/topic/libraries/architecture/navigation/navigation-implementing
In Jetpack Navigation Component, if you want to perform some operation when fragment is poped then you need to override following functions.
Add OnBackPressedCallback in fragment to run your special operation when back present in system navigation bar at bottom is pressed .
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
//perform your operation and call navigateUp
findNavController().navigateUp()
}
}
requireActivity().onBackPressedDispatcher.addCallback(onBackPressedCallback)
}
Add onOptionsItemMenu in fragment to handle back arrow press present at top left corner within the app.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setHasOptionsMenu(true)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
//perform your operation and call navigateUp
findNavController().navigateUp()
return true
}
return super.onOptionsItemSelected(item)
}
If there is no special code to be run when back is pressed on host fragment then use onSupportNavigateUp in Activity.
override fun onSupportNavigateUp(): Boolean {
if (navController.navigateUp() == false){
//navigateUp() returns false if there are no more fragments to pop
onBackPressed()
}
return navController.navigateUp()
}
Note that onSupportNavigateUp() is not called if the fragment contains onOptionsItemSelected()
As my back button works correctly, and using NavController.navigateUp() crashed on start destination back button. I have changed this code to something like this. Other possibility will be to just check if currentDestination == startDestination.id but I want to close Activity and go back to other Activity.
override fun onSupportNavigateUp() : Boolean {
//return findNavController(R.id.wizard_nav_host_fragment).navigateUp()
onBackPressed()
return true
}
/** in your activity **/
private boolean doubleBackToExitPressedOnce = false;
#RequiresApi(api = Build.VERSION_CODES.M)
#Override
public void onBackPressed() {
int start = Navigation.findNavController(this, R.id.nav_host_fragment).getCurrentDestination().getId();
if (start == R.id.nav_home) {
if (doubleBackToExitPressedOnce) {
super.onBackPressed();
return;
}
this.doubleBackToExitPressedOnce = true;
Toast.makeText(MainActivity.this, "Press back again to exits", Toast.LENGTH_SHORT).show();
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
doubleBackToExitPressedOnce = false;
}
}, 2000);
} else {
super.onBackPressed();
}
}
If you mean the start of your "root" navigation graph (just incase you have nested navigation graphs) then you shouldn't be showing an up button at all, at least according to the navigation principles.
Just call this in your back button Onclick
requireActivity().finish()
I am trying to implement KVO bindings in a Xamarin Mac desktop app.
I have followed the docs, and it is working, but the bindings appear to trigger 2 change events each time!
If I create a KVO model with a binding like this...
private int _MyVal;
[Export("MyVal")]
public int MyVal
{
get { return _MyVal; }
set
{
WillChangeValue("MyVal");
this._MyVal = value;
DidChangeValue("MyVal");
}
}
And bind a control to it in Xcode under the bindings section with the path self.SettingsModel.MyValue
It all appears to work fine, the control shows the model value, changing the model value programmatically updates the control and changing the control updates the model value.
However, it runs the change event twice.
I am listening to the change so I can then hit an API with the value.
SettingsModel.AddObserver(this, (NSString)key, NSKeyValueObservingOptions.New, this.Handle);
Then later...
public override void ObserveValue(NSString keyPath, NSObject ofObject, NSDictionary change, IntPtr context)
{
switch (keyPath)
{
case "MyValue":
// CODE HERE THAT UPDATES AN API WITH THE VALUE
// But this handler fires twice.
break;
}
}
Im not sure if its Xamarin or XCode that is causing the double trigger.
Interestingly, if you don't specify the Xcode WillChangeValue and DidChangeValue methods, then it doesn't trigger twice - as though Xamarin has automatically triggered the change once. However, it no longer triggers a change when programmatically updating the model value...
[Export("MyVal")]
public int MyVal { get; set }
The above will work for the Xcode controls, they will update the model and trigger a change event.
But programmatically updating it
this.SettingsModel.MyVal = 1;
Does not trigger the change event.
It's very confusing, any idea on how to stop 2 change events firing, as I don't want to hit the API twice every time!
When it fires twice, the stack trace (abridged) for the first has...
MainViewController.ObserveValue
ObjCRuntime.Messaging.void_objc_msgSendSuper_IntPtr()
Foundation.NSObject.DidChangeValue(string forKey)
CameraSettingsModel.set_MyValue(int value)
AppKit.NSApplication.NSApplicationMain()
AppKit.NSApplication.Main(string[] args)
MainClass.Main(string[] args)
Which looks fine, but the second...
MainViewController.ObserveValue
AppKit.NSApplication.NSApplicationMain()
AppKit.NSApplication.Main(string[] args)
MainClass.Main(string[] args)
Has no mention of the Setting Model triggering the event
You are hitting this - Receiving 2 KVO notifications for a single KVC change
and need to override AutomaticallyNotifiesObserversForKey it appears.
Cocoa is "doing you a favor" by sending the notifications for you, which is great except you have the managed version also sending notifications.
It looks something like this:
[Export ("automaticallyNotifiesObserversForKey:")]
public static new bool AutomaticallyNotifiesObserversForKey (string key) => false;
bool _checkValue;
[Export("CheckValue")]
public bool CheckValue
{
get { return _checkValue; }
set
{
WillChangeValue("CheckValue");
_checkValue = value;
DidChangeValue("CheckValue");
}
}
public override void ViewDidLoad ()
{
base.ViewDidLoad();
this.AddObserver("CheckValue", NSKeyValueObservingOptions.New, o =>
{
Console.WriteLine($"Observer triggered for {o}");
});
CheckValue = false;
}
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.
I need to solve a simple problem, but yet I have not been able to found out any solution yet.
I have a simple DropDownChoice with AJAX onChange() JS event. I need to add a confirm box before the onUpdate() action is done - this is not difficult, BUT I need to display the confirm box only if the new selected value of the DropDownChoice is X (one certain value), and do not display the confirm box in any other case. Is it doable?
Short example snippet:
DropDownChoice<Integer> choice = new DropDownChoice<Integer>("id", new Model<Integer>(0));
choice.add(new AjaxFormComponentUpdatingBehavior("onchange") {
#Override
protected void onUpdate(AjaxRequestTarget target) {
// do some stuff
}
#Override
protected void updateAjaxAttributes(AjaxRequestAttributes attributes) {
super.updateAjaxAttributes(attributes);
attributes.getAjaxCallListeners().add(new AjaxCallListener() {
#Override
public CharSequence getPrecondition(Component component) {
return "return confirm('Really?')"; // I NEED THIS DISPLAYED CONDITIONALLY
}
}
}
}
I don't know how to access the "choice" model object (converted input...) with the proposed value to add it to a condition in updateAjaxAttributes() method.
Thank you.
I think you should go for a JavaScript-based solution. The code of AJAX call listener is executed in a scope where you can use variable attrs. This variable contains the parameters used to perform AJAX call, including the id of the component. In this way you could check for selected value.
See more at http://wicket.apache.org/guide/guide/ajax.html#ajax_5
I need to recreate new page instance on every page load (also when user pressed Back button).
So I overrided OnBackKeyPress method:
protected override void OnBackKeyPress(CancelEventArgs e)
{
base.OnBackKeyPress(e);
if (NavigationService.CanGoBack) {
e.Cancel = true;
var j = NavigationService.RemoveBackEntry();
NavigationService.Navigate(j.Source);
NavigationService.RemoveBackEntry();
}
}
The problem is that I can't handle case when user press back button to close CustomMessageBox dialog. How can I check it? Or is there any way to force recreation of page instance when going back through history state?
Why do you need to recreate the page instance? If you are simply trying to re-read the data to be displayed, why not put the data loading logic into OnNavigatedTo()?
Assuming that is what you are actually trying to achieve, try something like this...
public partial class MainPage : PhoneApplicationPage
{
// Constructor
public MainPage()
{
InitializeComponent();
// don't do your data loading here. This will only be called on page creation.
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
LoadData();
base.OnNavigatedTo(e);
}
MyViewModel model;
async void LoadData()
{
model = new MyViewModel();
await model.LoadDataAsync();
}
}
If you also have specific logic that you need to run on first construction of the page vs. on a back key navigation, check the NavigationMode property of the NavigationEventArgs object that gets passed to OnNavigatedTo.
if(e.NavigationMode == NavigationMode.New)
{
//do what you need to do specifically for a new page instance
}
if (e.NavigationMode == NavigationMode.Back)
{
// do anything specific for back navigation here.
}
Ha, in the near thread, i have opposite question :)
What about MessageBox - it depends, which one are you using. It can be custom message box, for example. Anyway, try to check MessageBox.IsOpened (or alternative for your MessageBox) in your OnBackKeyPress().
Another solution is to use OnNavigatedTo() of the page you want to be new each time.
Third solution: in case you works with Mvvm Light, add some unique id in ViewModel getter, like
public MyViewModel MyViewModel
{
get
{
return ServiceLocator.Current.GetInstance<MyViewModel>((++Uid).ToString());
}
}
This would force to recreate new ViewModel each time, so you'd have different instance of VM, so you would have another data on the View.