How do I make sure that a CollectionView displays the last item i.e. the bottom one, from the start without the user having to scroll all the way down.
I use this in a chat UI that I created and want to make sure that the latest messages are in user's view right from the beginning and the user can always scroll up to see older messages.
Set a command in your VM that you can execute when the data is loaded or changed and listen for it in the view and do the scrolling there.
In your VM:
public Command CollectionUpdatedCommand { get; set; }
And then, once the collection changes, whenever you need to fire it:
CollectionUpdatedCommand?.Execute(null);
In your view, set the scroll code to execute when the command is executed:
viewModel.CollectionUpdatedCommand = new Command(() =>
{
Device.BeginInvokeOnMainThread(() => {
MessageList.ScrollTo(viewModel.Messages.Last(), null, ScrollToPosition.End, true);
});
});
You can do something like this along the OnAppearing() :
Device.BeginInvokeOnMainThread(() => {
MessageList.ScrollTo(viewModel.Messages.Last(), null, ScrollToPosition.End, true);
});`
Where MessageList - it's your CollectionView
and
viewModel.Messages - it's your Messages collection for this CollectionView e.g. ObservableCollection
Related
I have a viewmodel bound to a page. The viewmodel is updating a property reacting to an event. When the page in is in background (covered by other pages) it doesn't update its UI, even when i execute the viewmodel code on UI thread:
Device.BeginInvokeOnMainThread(() =>
{
MyProperty = false;
}
Observing this on Android at the moment.
Solved but my own answer below.
If i add a single line to this, it works, the page UI gets updated in background:
Device.BeginInvokeOnMainThread(async() =>
{
await Task.Delay(10);
MyProperty = false;
}
Is there any way to detect the press of the back button of the Navigation Page in Xamarin forms?
You can override your navigation page "OnBackButtonPressed" method:
protected override bool OnBackButtonPressed()
{
Device.BeginInvokeOnMainThread(async () =>
{
if (await DisplayAlert("Exit?", "Are you sure you want to exit from this page?", "Yes", "No"))
{
base.OnBackButtonPressed();
await App.Navigation.PopAsync();
}
});
return true;
}
If you are using the shell, you can override the Shell's OnNavigating event:
void OnNavigating(object sender, ShellNavigatingEventArgs e)
{
// Cancel back navigation if data is unsaved
if (e.Source == ShellNavigationSource.Pop && !dataSaved)
{
e.Cancel();
}
}
Update:
OnBackButtonPressed event will get fired ONLY on Android when user press the Hardware back button.
Seems like you are more interested to implement when any page get disappeared you want to do something!
In that case:
You have the page's two methods -
protected override void OnAppearing()
{
base.OnAppearing();
Console.WriteLine("Hey, Im coming to your screen");
}
protected override void OnDisappearing()
{
base.OnDisappearing();
Console.WriteLine("Hey, Im going from your screen");
}
You can override those 2 methods on any page to track when they appear and disappear.
Recent updates to Xamarin forms mean you can now do this in an application made with Shell Navigation for navigation back arrow on both platforms.
Use the Shell.SetBackButtonBehavior method, for example running this code in the constructor of your page object will allow the back navigation to take place only when the bound viewmodel is not busy:
Shell.SetBackButtonBehavior(this, new BackButtonBehavior
{
Command = new Command(async() =>
{
if (ViewModel.IsNotBusy)
{
await Shell.Current.Navigation.PopAsync();
}
})
});
In the body of the Command you can do whatever you need to do when you are intercepting the click of the back button.
Note that this will affect only the navigation back button, not the Android hardware back button - that will need handling separately as per the answers above. You could write a shared method called from both the back button pressed override and the command on shell back button behaviour places to share the logic.
You must override native navigationbar button behavior with custom renderer. OnBackButtonPressed triggers only physical device button. You can read good article how to achive this here
I have a Xamarin.Forms application which uses a TabbedPage, let's call it T, T consists of 3 ContentPage children A, B and C. Since the usere has the possibility to edit some data on tab B, I want to notify user before leaving tab in order to allow him to cancel the navigation change and save changes first or to discard changes and leave. So far I have managed to override OnBackButtonPressed() method and the navigation bar back button (which would exit TabbedPage). However I quickly noticed that I am still loosing changes when switching between tabs. I would like to override the click on new tab, so I could first present user with the leaving dialog and the skip the change or continue with it. What would be the best way to do this? I am currently working only on Android platform, so solutions on the platform level are also acceptible.
Thank you for your suggestions and feedback :)
I do not think there is an easy way to do this ,
you can use OnDissappearing and OnAppearing for the pages, that is as easy as it gets .
However I think you are using the wrong design.
Having tabs are ment to make it easier to navigate between pages, if you are going to notify the user when changing the tabs then it would be annoying . If I were you i would save the data for each page locally. so when you get back to the page you will have the data anyway.
So in the end I followed the advice of Ahmad and implemented the persisting of data on individual tabs so they are not lost when tabs are switched. (I no longer refresh input fields from data from model when OnAppearing is called).
But in order to know if there are some unsaved changes on my ChildB page, I had to implement the following procedures:
I created the method HandleExit on my ChildB page, which checks for unsaved changes in fields (at least one value in input fields is different from the ones in stored model) and the either prompts the user that there are unsaved changes (if there are some) or pops the navigation stack if there are no changes.
private async Task HandleExit()
{
if(HasUnsavedChanges())
{
var action = await DisplayAlert("Alert", "There are unsaved changes, do you want to discard them?", "Discard changes", "Cancel");
if(!action)
{
return;
}
}
await Navigation.PopAsync();
}
Since there are two ways on how user can return from Tabbed page (pressing the back button on device or pressing the back button in navigation bar, I had to:
A: override the back button method on my ChildB page, so it calls the HandleExit method. But since Navigation.PopAsync() needs to be called on UI thread, I had to explicitly execute the method on UI thread as written below:
protected override bool OnBackButtonPressed()
{
Device.BeginInvokeOnMainThread(new Action(async () =>
{
await HandleExit();
}));
return true;
}
B: Since there is no way to intercept the navigation bar back button on the ContentPage, I had to intercept the event on the platform level (Android) and then pass the event to the ContentPage if necessary via MessagingCenter. So first we need to intercept the event, when navigation bar button is pressed in one of the child pages and send the event via MessagingCenter. We can do that but adding the following method in our MainActivity.cs class:
public override bool OnOptionsItemSelected(IMenuItem item)
{
// check if the current item id
// is equals to the back button id
if (item.ItemId == 16908332)
{
// retrieve the current xamarin forms page instance
var currentpage = Xamarin.Forms.Application.Current.MainPage.Navigation.NavigationStack.LastOrDefault();
var name = currentpage.GetType().Name;
if(name == "ChildA" || name == "ChildB" || name == "ChildC")
{
MessagingCenter.Send("1", "NavigationBack");
return false;
}
}
return base.OnOptionsItemSelected(item);
}
Now whenever we will press the navigation bar back button in one of the child pages (ChildA, ChildB, ChildC) nothing will happen. But the button will work as before on the rest of the pages. For the second part of solution we need to handle the message from MessagingCenter, so we need to subscribe to it in our ChildB page. We can subsribe to the message topic in OnAppearing method as follows:
MessagingCenter.Subscribe<string>(this, "NavigationBack", async (arg) => {
await HandleExit();
});
Be careful to unsubscribe to the topic in OnDisappearing() otherwise strange things could happen, since there will be references left to your ContentPage even if you pop it from your navigation stack.
Now that we have handled both requests for back navigation in our ChildB page, we also need to handle them in all of remaining child pages (ChildA, ChildC), so they will know if there are unsaved changes in ChildB page, even if it is currently not selected. So the solution is again compraised of handling the device back button, and navigation bar back button, but first we heed a way to check if ChildB has unsaved changes when we are on one of the remaining pages, so we again write HandleExit method but this time it is as follows:
private async Task HandleExit()
{
var root = (TabbedPage)this.Parent;
var editPage = root.Children.Where(x => x.GetType() == typeof(ChildB)).FirstOrDefault();
if(editPage != null)
{
var casted = editPage as ChildB;
if (casted.HasUnsavedChanges())
{
var action = await DisplayAlert("Alert", "There are unsaved changes, do you want to discard them?", "Discard changes", "Cancel");
if (!action)
{
return;
}
}
}
await Navigation.PopAsync();
}
The only thing that remains now is to handle both navigation back events inside remaing child pages. The code for them is the same as in the actual ChildB page.
A: Handling the device back button.
protected override bool OnBackButtonPressed()
{
Device.BeginInvokeOnMainThread(new Action(async () =>
{
await HandleExit();
}));
return true;
}
B: Subscribing to topic from MessagingCenter
MessagingCenter.Subscribe<string>(this, "NavigationBack", async (arg) => {
await HandleExit();
});
If everthing has been done correctly, we should now be prompted with a dialog on any of the child pages if there are unsaved changes on the ChildB page. I hope this will help somebody in the future :)
I have an Xamarin.Forms app running on iOS simulator that just stops as soon as it hits code which adds a new member to an observable collection that is the template source for a CarouselView. Oddly, it doesnt happen immediately, but only when I add more items to the collection when the user gets close to the end of the carousel. I can't trap the error in a try catch block. In debug mode, when I stop at the line in question and either step in or step over, the app just closes and goes back to the home screen. I recently updated my packages, maybe there is some issue there?
Code as follows:
private ObservableCollection<ResultsScroller> mySource;
public void PopulateResultsPages(List<NominalResult> resultList)
{
foreach (var nr in resultList)
{
var template = new ResultsScroller();
template.LoadData(nr);
try
{
mySource.Add(template); // <----app quits here
}
catch (System.Exception e)
{
System.Diagnostics.Debug.WriteLine(e.Message);
}
}
lastPosition = mySource.Count - 1;
}
initialize observablecollevtion mySource in constructor like this
mySource = new ObservableCollection<ResultsScroller>();
after that add item in it.
Currently you have to somehow programmatically make the ListView appear before adding an item. If the ListView is inside a tab that is not visible, show it by setting the CurrentPage Property. If it is in a MasterDetailPage, you could try showing it by toggling the IsPresented Property.
An upcoming Update of XF should fix this issue:
- https://github.com/xamarin/Xamarin.Forms/issues/1542
- https://github.com/xamarin/Xamarin.Forms/issues/1927
I have a list view that I am popping up in Xamarin forms, that I want to hide if someone taps outside of the box. I have a tap gesture recognizer on the parent layout for the list view that handles that. In Android, it all works good. If I click off, it closes, but if I click on an element in the list view, it properly selects it. In iOS, the opposite happens. The gesture handler on the layout fires first and closes the list view without properly selecting the item.
So my question, is there a way to change the order on how the events are fired? If not, is there a better alternative to how I'm trying to accomplish this? Thanks!
If you are using ListView.ItemSelected or ListView.ItemTapped then I ran into the exact same issue the other day. The fix for me was to not use either of those and instead attach a TapGestureRecognizer to the ViewCell that is within the ListView. I also added an IsSelected property to the object that the ViewCell is being bound to so that I could change the background color of the item once it has been clicked.
public class SomePage : ContentPage {
private SomeModel _selectedModel; //It would be best to put this into your ViewModel
...
public SomePage() {
ListView list = new ListView {
ItemTemplate = new DataTemplate(() => {
ViewCell cell = new ViewCell {
View = new ContentView()
};
cell.View.GestureRecognizers.Add(new TapGestureRecognizer {
Command = new Command(() => {
if(_selectedModel != null) { _selectedModel.IsSelected = false; }
SomeModel model = (SomeModel)cell.BindingContext;
model.IsSelected = true;
_selectedModel = model;
})
}
return cell;
}
}
}
}