SolidSnake4444
SolidSnake4444

Reputation: 3587

Xamarin Android - How do I pass an event from MainActivity to ViewModel on Forms Page?

I have a Xamarin Forms app that I want to read NFC tags on. I've made an interface called INFC for reading the tags.

/// <summary>
/// This interface defines NFC relating functions that are cross platform.
/// </summary>
public interface INFC
{
    /// <summary>
    /// Inits the object.
    /// </summary>
    void Init();

    /// <summary>
    /// Starts the process for scanning for the included platform.
    /// </summary>       
    /// <param name="tagInformation">Optional related tag information that you may need for the scan.</param>
    void StartNFCScan(object tagInformation = null);

    /// <summary>
    /// Called when the tag is finished scanning and we have the content.
    /// </summary>
    event EventHandler<String> TagScanned;
}

I created the following Android specific implementation.

[assembly: Dependency(typeof(INFCImplementation))]
namespace Test.Droid.Models
{
/// <summary>
/// The android implementation of the NFC platform.
/// </summary>
public class INFCImplementation : INFC
{        
    public event EventHandler<String> TagScanned;
    public static NfcAdapter adapter { get; set; }

    /// <summary>
    /// Called to init the object.
    /// </summary>        
    public void Init()
    {
        //Set the adapter.
        adapter = NfcAdapter.GetDefaultAdapter(Forms.Context);
    }

    /// <summary>
    /// Starts the process for scanning for the included platform.
    /// </summary>       
    /// <param name="tagInformation">Optional related tag information that you may need for the scan.</param>
    public void StartNFCScan(object tagInformation = null)
    {
        //Create a variable to hold the tag content.
        String tagContent = null;

        try
        {                
            //Process the NDEF tag and get the content as a String.
            tagContent = "http://stackoverflow.com";
        }
        catch (Exception e)
        {
        }

        //Raise the tag content with the scanned event.
        TagScanned?.Invoke(this, tagContent);
    }
}

}

My Main Activity is as follows.

/// <summary>
/// The main activity for the app.
/// </summary>
[Activity(Label = "Test", Icon = "@drawable/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
    INFCImplementation nfcImplementation;        

    protected override void OnCreate(Bundle bundle)
    {
        TabLayoutResource = Resource.Layout.Tabbar;
        ToolbarResource = Resource.Layout.Toolbar;

        base.OnCreate(bundle);

        //Enable experimental fast renderers.
        Forms.SetFlags("FastRenderers_Experimental");

        Forms.Init(this, bundle);

        //Load up the zxing framework.
        ZXing.Net.Mobile.Forms.Android.Platform.Init();

        //Load up the user dialogs plugin.
        UserDialogs.Init(() => (Activity)Forms.Context);

        //Init the tinted image renderer.
        TintedImageRenderer.Init();            

        //Store our NFC interface class.
        nfcImplementation = DependencyService.Get<INFCImplementation>() as INFCImplementation;

        //Init our NFC interface.
        nfcImplementation.Init();            

        LoadApplication(new App());
    }

    protected override void OnResume()
    {
        //Call the base method.
        base.OnResume();

        //Create the intent for NFC reading.
        Intent intent = new Intent(this, GetType()).AddFlags(ActivityFlags.SingleTop);

        //Start a dispatch on our NFC adapter.
        INFCImplementation.adapter?.EnableForegroundDispatch
        (
            this,
            PendingIntent.GetActivity(this, 0, intent, 0),
            new[] { new IntentFilter(NfcAdapter.ActionTechDiscovered) },
            new String[][]
            {
                new string[]
                {
                    "android.nfc.tech.Ndef"
                },
                new string[] {
                        "android.nfc.tech.MifareClassic"
                    },
            }
        );
    }

    protected override void OnPause()
    {
        //Call the base method.
        base.OnPause();

        //Stop the dispatch on our NFC adapter.
        INFCImplementation.adapter?.DisableForegroundDispatch(this);
    }

    protected override void OnNewIntent(Intent intent)
    {
        //Call the base method.
        base.OnNewIntent(intent);

        //Check if this is the NFC intent.
        if (intent != null && (NfcAdapter.ActionNdefDiscovered.Equals(intent.Action) || NfcAdapter.ActionTechDiscovered.Equals(intent.Action) || NfcAdapter.ActionTagDiscovered.Equals(intent.Action)))
        {
            var test = intent.GetParcelableExtra(NfcAdapter.ExtraTag) as Tag;
            nfcImplementation.StartNFCScan(test);
        }
    }

    public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults)
    {
        //Call the base method.
        base.OnRequestPermissionsResult(requestCode, permissions, grantResults);

        //Check with the permissions plugin.
        PermissionsImplementation.Current.OnRequestPermissionsResult(requestCode, permissions, grantResults);

        //Check with the zxing plugin.
        ZXing.Net.Mobile.Android.PermissionsHandler.OnRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

In my view model for the mainpage that is the binding context for the main page I add the following in the constructor.

    /// <summary>
    /// Constructs the scanner view model with the scanner view we want to use.
    /// </summary>
    public ScannerPageViewModel()
    {
        //Subscribe to the tag scanned event.
        CrossNFC.Current.TagScanned += ProcessNFCScanResult;
    }

    private void ProcessNFCScanResult(object sender, string e)
    {
        SetLabel(e);
    }

Ok so for the issue. I believe that this should make it so the OnNewIntent function will call the start NFC scan on the interface and then that will call the event which will fly all the way up to the view model and allow me to handle the content. I do this as I only want to scan NFC tags on one page in the app ONLY while the app is in the foreground. Everytime I get to the invoke call, the TagScanned event is null.

Placing breakpoints around I found that the following occurs when I scan a tag:

MainActivity OnPause Gets Called -> Scanner Page OnDisappearing Gets Called -> OnNewIntent Gets Called and calls the null event -> MainActivity OnResume gets called -> Scanner Page OnAppearing Gets Called

I believe that the OnDisappearing call is making the event unable to process. I based a lot of my code on the NFCForms Github project (https://github.com/poz1/NFCForms) which when downloading the sample project and running it DOES NOT trigger OnDisappearing and OnAppearing. It just calls OnPause, OnNewIntent, and OnResume and the event gets to his page.

Why is my page getting unloaded and the event doesn't get called? If I'm doing something wrong, how can I notify my ViewModel for the specific page when a tag was scanned? I'm thinking this is either an issue with the way I'm making the intent request for NFC or something not related to NFC in which I'm processing view events wrong due to the sample NFCForms application working properly on the same phone.

Edit

I made a completely new project with the same basic code and it worked the way I believe it should have. Now I'm trying to figure out why OnAppearing and OnDisappearing is called on the pages.

Edit 2

I found out that OnAppearing and OnDisappearing gets called if the pages are wrapped in a navigation page. That is why the new project which is a single view did not call it and when I added the navigation page it did call it.

HOWEVER even when changing my project to a single page, the old project I'm working on still had the event as null, while the new test project had the event as valid.

So I'm thinking that somehow I'm not doing events right?

Upvotes: 3

Views: 6680

Answers (3)

Clint StLaurent
Clint StLaurent

Reputation: 1268

Last successful post was Jan 2018. Anyone have this working in 2019? Every test I try the anonymous MessagingCenter message is never heard by the subscriber.

Same use-case as the original poster: Send the message from MainActivty of Android, listen for it in a ViewModel of the shared/agnostic layer.

In my tests, the anonymous message isn't even heard within the same class, or same layer.

UPDATE: Further input from the community found a solution: You have to specify the type Application and Application.Current. https://forums.xamarin.com/discussion/comment/370364#Comment_370364

So the previously working syntax seems broken in 2019 - there is a way around it.

Android layer (MainActivity or Broadcast listener): MessagingCenter.Send(Xamarin.Forms.Application.Current,"ISENGINEON", result.ToString()); Shared layer view model MessagingCenter.Subscribe<Application,string>(this,"ISENGINEON",OnEngineOnChanged);

Upvotes: 3

SolidSnake4444
SolidSnake4444

Reputation: 3587

While an answer like this makes me very sad, it did end up working.

Since I found making a new project worked, I made a new forms project with the same name as mine and then removed the Android project from my solution and replaced it with the new one. I then reinstalled all the nuget packages and copied and pasted all my code in the new project.

And it works now.....

So I'm guessing something along the way broke something in the core of the VS project or something. I hate kinda hand wavy answers but this is what worked for me. All the code I posted above was unchanged and it started working.

Upvotes: 0

Greggz
Greggz

Reputation: 1809

Would this be of some use to your case ?

// In ScannerPage
protected override void OnAppearing ()
{
    base.OnAppearing ();
    MessagingCenter.Subscribe<string>(this, "eventName", (label) => {
        // do something whenever the message is sent
        Device.BeginInvokeOnMainThread (() => {
           MyScannerPageViewModel.SetLabel(label);
        });
    });
}

protected override void OnDisappearing ()
{
    base.OnDisappearing ();
    MessagingCenter.Unsubscribe<string> (this, "eventName");
}

And in the MainActivity chose where you would like to put this line

Xamarin.Forms.MessagingCenter.Send("LabelName","eventName");

EDIT: Changed the code a bit

Upvotes: 4

Related Questions