RecyclerView Click event - xamarin

I have created a RecyclerView adapter and I'm trying to start an activity when a row is clicked:
public override OnBindViewHolder(RecyclerView.ViewHolder holder, int position)
{
MyViewHolder viewHolder = (MyViewHolder)holder;
viewHolder.MyView.Click += (sender, e) =>
{
var context = viewHolder.MyView.Context;
var intent = new Intent(context, typeof(DetailActivity));
context.StartActivity(intent);
}
}
When I click the first row it will take me to the activity like I want. If I scroll down so that the first row is rebound and then scroll back to the top again and then click the first row then my Click event fires twice. Once for the first row that was bound and then again for a row that was bound when I scrolled.
Is there an event you need to handle to unregister the click events?

I believe the standard pattern is to setup your clickhandlers in the constructor of the ViewHolder. Then in OnBindViewHolder, you update the Views/Data inside the ViewHolder.
Something like this (not compiled code):
Adapter:
public override OnBindViewHolder()
{
MyViewHolder viewHolder = (MyViewHolder)holder;
viewHolder.SetData(whatever data you care about);
}
MyViewHolder:
public MyViewHolder(View view) : base(view)
{
MainView = view;
MainView.Click += (sender, e) =>
{
var context = MainView.Context;
var intent = new Intent(context, typeof(DetailActivity));
context.StartActivity(intent);
}
}
Doing it this way keeps the Adapter cleaner by putting business logic in the ViewHolder, and also prevents your click handlers from being constantly setup and torn down as you scroll.

Related

How can I open another activity using spinner item selection in xamarin.forms only?

I've tried this and it looks like majority of search results reference to Android studio. I'm using visual studio, xamarin forms.
protected override void OnCreate (Bundle bundle)
{
base.OnCreate (bundle);
SetContentView (Resource.Layout.Main);
Spinner spinner = FindViewById<Spinner> (Resource.Id.spinner);
spinner.ItemSelected += new EventHandler<AdapterView.ItemSelectedEventArgs> (spinner_ItemSelected);
var adapter = ArrayAdapter.CreateFromResource (
this, Resource.Array.my_array, Android.Resource.Layout.SimpleSpinnerItem);
adapter.SetDropDownViewResource (Android.Resource.Layout.SimpleSpinnerDropDownItem);
spinner.Adapter = adapter;
}
The spinner loads perfectly but the item selected method opens the activity on loading.
private void spinner_ItemSelected (object sender, AdapterView.ItemSelectedEventArgs e)
{
SetContentView (Resource.Layout.page1);
}
How best can I load the activity on specific item selection. Note: the items are referenced in the Strings.xml.
Because Spinner chooses the first item by default when initialized, it will fire spinner_ItemSelected
You can add a conditional judgment to your spinner_ItemSelected method:
private void spinner_ItemSelected (object sender, AdapterView.ItemSelectedEventArgs e)
{
var index = e.Parent.SelectedItemPosition; //base on the select position
var obj = e.Parent.SelectedItem; // base on the selectitem value(string)
// xxx is your conditions
if(index == xxx)
{
SetContentView (Resource.Layout.page1);
}
// or
if(obj.ToString().Equals("xxx"))
{
SetContentView (Resource.Layout.page1);
}
}

Xamarin Forms : Add ItemSource on focus

Am trying to load ItemSource of a picker when the picker is focused.
But the data is not loaded on 1st focus.
here is the code sample
List<object> itmSrc;
Picker picker = new Picker();
itmSrc = Controls[i].ItemSource;
picker.Focused += BindItemSourceOnFocus;
public void BindItemSourceOnFocus(object sender, FocusEventArgs e)
{
var p = e.VisualElement as Picker;
p.ItemsSource = itmSrc;
}
If any other different approach is possible, let me know.
You can do it adding items on an async method, or another thread. Load the data on view focus is just transferring the issue to another place, and it gives a bad user experience at all.
If you run a code block inside a Task.Run(), for example, this code will be executed on another thread, and the interface should not hang on data loading.
Something like this:
public class MyPage : ContentPage
{
List<object> itmSrc;
Picker picker;
public MyPage()
{
// Your stuff goes here
itmSrc = new List<object>();
picker = new Picker();
StackLayout content = new StackLayout();
content.Crindren.Add(picker);
this.Content = content;
Task.Run(() => LoadData());
}
private void LoadData()
{
// Get your data from anywhere and put it on the itemSrc from here.
// Then...
picker.ItemsSource = itmSrc;
}
}
I hope it helps.

Xamarin memory leakage

I'm a newbie to Xamarin. I have created an application that uses DrawerLayout(Android). But my problem is that every time i select a item in the menu(DrawerLayout menu), the memory increases, and this causes the app to become slow and crush. I've tried to use Xamarin profiler to analyze memory leaks - it suspects something called String.FastAllocationString, but it doesn't really show the line(code) that causes String.FastAllocationString issue. Please help ? Here is my code :
MainActivity
DrawerLayout drawerLayout;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
// Set our view from the "main" layout resource
SetContentView(Resource.Layout.Main);
drawerLayout = FindViewById<DrawerLayout>(Resource.Id.drawer_layout);
// Init toolbar
var toolbar = FindViewById<Android.Support.V7.Widget.Toolbar>(Resource.Id.app_bar);
SetSupportActionBar(toolbar);
SupportActionBar.SetTitle(Resource.String.app_name);
SupportActionBar.SetDisplayHomeAsUpEnabled(true);
SupportActionBar.SetDisplayShowHomeEnabled(true);
// Attach item selected handler to navigation view
var navigationView = FindViewById<NavigationView>(Resource.Id.nav_view);
navigationView.NavigationItemSelected += NavigationView_NavigationItemSelected;
// Create ActionBarDrawerToggle button and add it to the toolbar
var drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, Resource.String.open_drawer, Resource.String.close_drawer);
drawerLayout.SetDrawerListener(drawerToggle);
drawerToggle.SyncState();
}
void NavigationView_NavigationItemSelected(object sender, NavigationView.NavigationItemSelectedEventArgs e)
{
var ft = FragmentManager.BeginTransaction();
ft.AddToBackStack(null);
switch (e.MenuItem.ItemId)
{
case (Resource.Id.nav_incidents):
SupportActionBar.SetTitle(Resource.String.toolbar_Test);
ft.Add(Resource.Id.HomeFrameLayout, new Test());
break;
}
ft.Commit();
ft.Dispose();
// Close drawer
drawerLayout.CloseDrawers();
}
Fragment
[Activity(Label = "Test")]
public class Test : Fragment
{
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View view = inflater.Inflate(Resource.Layout.Test, container, false);
return view;
}
}
Xamarin Profiler
you have to check fragment is available before you add new one
switch (e.MenuItem.ItemId)
{
case (Resource.Id.nav_incidents):
SupportActionBar.SetTitle(Resource.String.toolbar_Test);
Fragment myFragment =
(Fragment)FragmentManager.FindFragmentByTag("FRAGMENT1");
if (myFragment.IsVisible){
ft.Replace(Resource.Id.HomeFrameLayout, new Test(),"FRAGMENT1");
}else{
ft.Add(Resource.Id.HomeFrameLayout, new Test(),"FRAGMENT1");
}
break;
}
Hope this help

Xamarin.Android: Click event on Textview inside list view cell called multiple times

I have a list view in which inside each cell I have two textview, I need to handle the click event for one of the textview element. But when I put the click event inside the GetView() of the adapter, it is called multiple times.
public override View GetView(int position, View convertView, ViewGroup parent)
{
var item = tableItems[position];
ViewHolder holder;
View view = convertView;
if (view == null)
{
LayoutInflater layoutInflator = LayoutInflater.From(mContext);
view = layoutInflator.Inflate(Resource.Layout.myListViewCell, null);
holder = new ViewHolder();
holder.tvEmpName = view.FindViewById<TextView>(Resource.Id.tv_EmpName);
holder.tvEmpPhone = view.FindViewById<TextView>(Resource.Id.tv_EmpPhone);
view.Tag = holder;
}
else
{
holder = (ViewHolder)view.Tag;
}
holder.tvEmpName.Text = item.FullName;
holder.tvEmpPhone.Text = item.Phone;
holder.tvEmpPhone.Click += (sender, e) => {
// Click event to launch the Popup menu
// This event is being called multiple times, as Get view() being called multiple times.
};
return view;
}
I gone through this similar thread, but didn't find any solution.
The event is called multiple times because evertime when the metod getview is called you add a click event with +=.
In this case you can put the click event inside the if, like that:
if (view == null)
{
LayoutInflater layoutInflator = LayoutInflater.From(mContext);
view = layoutInflator.Inflate(Resource.Layout.myListViewCell, null);
holder = new ViewHolder();
holder.tvEmpName = view.FindViewById<TextView>(Resource.Id.tv_EmpName);
holder.tvEmpPhone = view.FindViewById<TextView>(Resource.Id.tv_EmpPhone);
view.Tag = holder;
holder.tvEmpPhone.Click += (sender, e) => {
// Code here
};
}
But the recyclerview is better than listview for this.

How to handle/cancel back navigation in Xamarin Forms

I tried to use the back navigation by overriding OnBackButtonPressed, but somehow it wasn't get called at all. I am using the ContentPage and the latest 1.4.2 release.
Alright, after many hours I figured this one out. There are three parts to it.
#1 Handling the hardware back button on android. This one is easy, override OnBackButtonPressed. Remember, this is for a hardware back button and android only. It will not handle the navigation bar back button. As you can see, I was trying to back through a browser before backing out of the page, but you can put whatever logic you need in.
protected override bool OnBackButtonPressed()
{
if (_browser.CanGoBack)
{
_browser.GoBack();
return true;
}
else
{
//await Navigation.PopAsync(true);
base.OnBackButtonPressed();
return true;
}
}
#2 iOS navigation back button. This one was really tricky, if you look around the web you'll find a couple examples of replacing the back button with a new custom button, but it's almost impossible to get it to look like your other pages. In this case I made a transparent button that sits on top of the normal button.
[assembly: ExportRenderer(typeof(MyAdvantagePage), typeof
(MyAdvantagePageRenderer))]
namespace Advantage.MyAdvantage.MobileApp.iOS.Renderers
{
public class MyAdvantagePageRenderer : Xamarin.Forms.Platform.iOS.PageRenderer
{
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
if (((MyAdvantagePage)Element).EnableBackButtonOverride)
{
SetCustomBackButton();
}
}
private void SetCustomBackButton()
{
UIButton btn = new UIButton();
btn.Frame = new CGRect(0, 0, 50, 40);
btn.BackgroundColor = UIColor.Clear;
btn.TouchDown += (sender, e) =>
{
// Whatever your custom back button click handling
if (((MyAdvantagePage)Element)?.
CustomBackButtonAction != null)
{
((MyAdvantagePage)Element)?.
CustomBackButtonAction.Invoke();
}
};
NavigationController.NavigationBar.AddSubview(btn);
}
}
}
Android, is tricky. In older versions and future versions of Forms once fixed, you can simply override the OnOptionsItemselected like this
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 = (MyAdvantagePage)
Xamarin.Forms.Application.
Current.MainPage.Navigation.
NavigationStack.LastOrDefault();
// check if the page has subscribed to
// the custom back button event
if (currentpage?.CustomBackButtonAction != null)
{
// invoke the Custom back button action
currentpage?.CustomBackButtonAction.Invoke();
// and disable the default back button action
return false;
}
// if its not subscribed then go ahead
// with the default back button action
return base.OnOptionsItemSelected(item);
}
else
{
// since its not the back button
//click, pass the event to the base
return base.OnOptionsItemSelected(item);
}
}
However, if you are using FormsAppCompatActivity, then you need to add onto your OnCreate in MainActivity this to set your toolbar:
Android.Support.V7.Widget.Toolbar toolbar = this.FindViewById<Android.Support.V7.Widget.Toolbar>(Resource.Id.toolbar);
SetSupportActionBar(toolbar);
But wait! If you have too old a version of .Forms or too new version, a bug will come up where toolbar is null. If this happens, the hacked together way I got it to work to make a deadline is like this. In OnCreate in MainActivity:
MobileApp.Pages.Articles.ArticleDetail.androdAction = () =>
{
Android.Support.V7.Widget.Toolbar toolbar = this.FindViewById<Android.Support.V7.Widget.Toolbar>(Resource.Id.toolbar);
SetSupportActionBar(toolbar);
};
ArticleDetail is a Page, and androidAction is an Action that I run on OnAppearing if the Platform is Android on my page. By this point in your app, toolbar will no longer be null.
Couple more steps, the iOS render we made above uses properties that you need to add to whatever page you are making the renderer for. I was making it for my MyAdvantagePage class that I made, which implements ContentPage . So in my MyAdvantagePage class I added
public Action CustomBackButtonAction { get; set; }
public static readonly BindableProperty EnableBackButtonOverrideProperty =
BindableProperty.Create(
nameof(EnableBackButtonOverride),
typeof(bool),
typeof(MyAdvantagePage),
false);
/// <summary>
/// Gets or Sets Custom Back button overriding state
/// </summary>
public bool EnableBackButtonOverride
{
get
{
return (bool)GetValue(EnableBackButtonOverrideProperty);
}
set
{
SetValue(EnableBackButtonOverrideProperty, value);
}
}
Now that that is all done, on any of my MyAdvantagePage I can add this
:
this.EnableBackButtonOverride = true;
this.CustomBackButtonAction = async () =>
{
if (_browser.CanGoBack)
{
_browser.GoBack();
}
else
{
await Navigation.PopAsync(true);
}
};
That should be everything to get it to work on Android hardware back, and navigation back for both android and iOS.
You are right, in your page class override OnBackButtonPressed and return true if you want to prevent navigation. It works fine for me and I have the same version.
protected override bool OnBackButtonPressed()
{
if (Condition)
return true;
return base.OnBackButtonPressed();
}
Depending on what exactly you are looking for (I would not recommend using this if you simply want to cancel back button navigation), OnDisappearing may be another option:
protected override void OnDisappearing()
{
//back button logic here
}
OnBackButtonPressed() this will be called when a hardware back button is pressed as in android. This will not work on the software back button press as in ios.
Additional to Kyle Answer
Set
Inside YOURPAGE
public static Action SetToolbar;
YOURPAGE OnAppearing
if (Device.RuntimePlatform == Device.Android)
{
SetToolbar.Invoke();
}
MainActivity
YOURPAGE.SetToolbar = () =>
{
Android.Support.V7.Widget.Toolbar toolbar =
this.FindViewById<Android.Support.V7.Widget.Toolbar>(Resource.Id.toolbar);
SetSupportActionBar(toolbar);
};
I use Prism libray and for handle the back button/action I extend INavigatedAware interface of Prism on my page and I implement this methods:
public void OnNavigatedFrom(INavigationParameters parameters)
{
if (parameters.GetNavigationMode() == NavigationMode.Back)
{
//Your code
}
}
public void OnNavigatedTo(INavigationParameters parameters)
{
}
Method OnNavigatedFrom is raised when user press back button from Navigation Bar (Android & iOS) and when user press Hardware back button (only for Android).
For anyone still fighting with this issue - basically you cannot intercept back navigation cross-platform. Having said that there are two approaches that effectively solve the problem:
Hide the NavigationPage back button with NavigationPage.ShowHasBackButton(this, false) and push a modal page that has a custom Back/Cancel/Close button
Intercept the back navigation natively for each platform. This is a good article that does it for iOS and Android: https://theconfuzedsourcecode.wordpress.com/2017/03/12/lets-override-navigation-bar-back-button-click-in-xamarin-forms/
For UWP you are on your own :)
Edit:
Well, not anymore since I did it :) It actually turned out to be pretty easy – there is just one back button and it’s supported by Forms so you just have to override ContentPage’s OnBackButtonPressed:
protected override bool OnBackButtonPressed()
{
if (Device.RuntimePlatform.Equals(Device.UWP))
{
OnClosePageRequested();
return true;
}
else
{
base.OnBackButtonPressed();
return false;
}
}
async void OnClosePageRequested()
{
var tdvm = (TaskDetailsViewModel)BindingContext;
if (tdvm.CanSaveTask())
{
var result = await DisplayAlert("Wait", "You have unsaved changes! Are you sure you want to go back?", "Discard changes", "Cancel");
if (result)
{
tdvm.DiscardChanges();
await Navigation.PopAsync(true);
}
}
else
{
await Navigation.PopAsync(true);
}
}
protected override bool OnBackButtonPressed()
{
base.OnBackButtonPressed();
return true;
}
base.OnBackButtonPressed() returns false on click of hardware back button.
In order to prevent operation of back button or prevent navigation to previous page. the overriding function should be returned as true. On return true, it stays on the current xamarin form page and state of page is also maintained.
The trick is to implement your own navigation page that inherits from NavigationPage. It has the appropriate events Pushed, Popped and PoppedToRoot.
A sample implementation could look like this:
public class PageLifetimeSupportingNavigationPage : NavigationPage
{
public PageLifetimeSupportingNavigationPage(Page content)
: base(content)
{
Init();
}
private void Init()
{
Pushed += (sender, e) => OpenPage(e.Page);
Popped += (sender, e) => ClosePage(e.Page);
PoppedToRoot += (sender, e) =>
{
var args = e as PoppedToRootEventArgs;
if (args == null)
return;
foreach (var page in args.PoppedPages.Reverse())
ClosePage(page);
};
}
private static void OpenPage(Page page)
{
if (page is IPageLifetime navpage)
navpage.OnOpening();
}
private static void ClosePage(Page page)
{
if (page is IPageLifetime navpage)
navpage.OnClosed();
page.BindingContext = null;
}
}
Pages would implement the following interface:
public interface IPageLifetime
{
void OnOpening();
void OnClosed();
}
This interface could be implemented in a base class for all pages and then delegate it's calls to it's view model.
The navigation page and could be created like this:
var navigationPage = new PageLifetimeSupportingNavigationPage(new MainPage());
MainPage would be the root page to show.
Of course you could also just use NavigationPage in the first place and subscribe to it's events without inheriting from it.
Maybe this can be usefull, You need to hide the back button, and then replace with your own button:
public static UIViewController AddBackButton(this UIViewController controller, EventHandler ev){
controller.NavigationItem.HidesBackButton = true;
var btn = new UIBarButtonItem(UIImage.FromFile("myIcon.png"), UIBarButtonItemStyle.Plain, ev);
UIBarButtonItem[] items = new[] { btn };
controller.NavigationItem.LeftBarButtonItems = items;
return controller;
}
public static UIViewController DeleteBack(this UIViewController controller)
{
controller.NavigationItem.LeftBarButtonItems = null;
return controller;
}
Then call them into these methods:
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
this.AddBackButton(DoSomething);
UpdateFrames();
}
public override void ViewWillDisappear(Boolean animated)
{
this.DeleteBackButton();
}
public void DoSomething(object sender, EventArgs e)
{
//Do a barrel roll
}
Another way around is to use Rg.Plugins.Popup Which allows you to implement nice popup. It uses another NavigationStack => Rg.Plugins.Popup.Services.PopupNavigation.Instance.PopupStack. So your page won't be wrap around the NavigationBar.
In your case I would simply
Create a full page popup with opaque background
Override ↩️ OnBackButtonPressed for Android on ⚠️ParentPage⚠️ with something like this:
protected override bool OnBackButtonPressed()
{
return Rg.Plugins.Popup.Services.PopupNavigation.Instance.PopupStack.Any();
}
Since the back-button affect the usual NavigationStack your parent would pop out whenever the user try to use it while your "popup is showing".
Now what? Xaml what ever you want to properly close your popup with all the check you want.
💥 Problem solved for these targets💥
[x] Android
[x] iOS
[-] Windows Phone (Obsolete. Use v1.1.0-pre5 if WP is needed)
[x] UWP (Min Target: 10.0.16299)

Resources