2021年1月11日 星期一

透過 Xamarin.Forms 來讀取 NFC 裝置 Tag

透過 Xamarin.Forms 來讀取 NFC 裝置 Tag

在這篇文章將會逐步說明,如何使用 Plugin.NFC 套件,進行讀取 NFC 標籤 Tag 內碼資訊功能。

這篇專案的原始碼可以在 Github XFNFC 找到

建立專案

  • 開啟 Visual Studio 2019
  • 在 [Visual Studio 2019] 對話窗中,選擇 [建立新的專案]
  • 在 [建立新專案] 對話窗,選擇 [Prism Blank App (Xamarin.Forms)]
  • 點選 [下一步] 按鈕
  • 在 [設定新的專案] 對話窗, [專案名稱] 欄位輸入 XFNFC
  • 點選 [建立] 按鈕
  • 在 [RPISM PROJECT WIZARD] 對話窗內,勾選 [ANDROID] & [iOS]
  • 點選 [CREATE PROJECT] 按鈕

加入新套件

安裝 PropertyChanged.Fody

  • 在 [XFNFC] 專案內
  • 滑鼠右擊 [相依性節點],點選 [管理 NuGet 套件]
  • 當 [NuGet: XFNFC] 視窗出現後,點選 [瀏覽] 標籤頁次
  • 在文字輸入盒內輸入 PropertyChanged.Fody ,搜尋出這個套件
  • 點選 [PropertyChanged.Fody] 這個套件,安裝到 Xamarin.Forms 專案內

安裝 Plugin.NFC

  • 在 [XFNFC] 專案內
  • 滑鼠右擊 [相依性節點],點選 [管理 NuGet 套件]
  • 當 [NuGet: XFNFC] 視窗出現後,點選 [瀏覽] 標籤頁次
  • 在文字輸入盒內輸入 Plugin.NFC ,搜尋出這個套件
  • 點選 [Plugin.NFC] 這個套件,安裝到 Xamarin.Forms 專案內

更新所有的套件(選擇性)

  • 滑鼠右擊 [方案總管] 最上方的節點 [解決方案 xfMediaPicker]
  • 點選 [管理方案的 NuGet 套件]
  • 當 [NuGet: 解決方案] 視窗出現後,點選 [更新] 標籤頁次
  • 點選 [選取所有封裝] ,接著點選 [更新按鈕]
  • 將選取套件更新到最新版本

Android 原生專案的設定

  • 在 [XFNFC.Android] 專案內的 [Properties] 節點內
  • 打開 [AssemblyInfo.cs] 檔案
  • 加入底下的敘述在該檔案的最後面
[assembly: UsesPermission(Android.Manifest.Permission.Nfc)]
[assembly: UsesFeature("android.hardware.nfc", Required = false)]
  • 打開 [MainActivity.cs] 檔案
  • 找到 [OnCreate] 方法
  • 在 [base.OnCreate] 方法呼叫之後,加入 CrossNFC.Init(this); 敘述
  • 完成後的結果如下
protected override void OnCreate(Bundle savedInstanceState)
{
    TabLayoutResource = Resource.Layout.Tabbar;
    ToolbarResource = Resource.Layout.Toolbar;
 
    base.OnCreate(savedInstanceState);
 
    // https://github.com/franckbour/Plugin.NFC
    // Plugin NFC: Initialization
    CrossNFC.Init(this);
 
    Xamarin.Essentials.Platform.Init(this, savedInstanceState);
    global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
    LoadApplication(new App(new AndroidInitializer()));
}
  • 在 [MainActivity] 類別內
  • 加入底下的方法宣告
protected override void OnNewIntent(Intent intent)
{
    base.OnNewIntent(intent);
    CrossNFC.OnNewIntent(intent);
}
 
protected override void OnResume()
{
    base.OnResume();
 
    // Plugin NFC: Restart NFC listening on resume (needed for Android 10+) 
    CrossNFC.OnResume();
}
  • 在 [MainActivity] 類別外
  • 加入底下的 Attribute 屬性宣告

請注意,這裡的 [DataMimeType] 宣告為 com.companyname.MyNFC ,若要套用此套件功能到開發中的專案,在此,請修改為該 Android 專案用的套件名稱

    [IntentFilter(new[] { NfcAdapter.ActionNdefDiscovered }, 
        Categories = new[] { Intent.CategoryDefault }, 
        DataMimeType = "application/com.companyname.MyNFC")]

iOS 原生專案設定

  • 在 iOS 專案 [XFNFC.iOS]
  • 使用滑鼠右擊 [Entitlements.plist] 檔案
  • 選擇 [開啟方式] 選項
  • 從 [開啟方式 - Entitlements.plist] 對話窗內
  • 選擇 [XML (文字) 編輯器]
  • 點選 [確定] 按鈕
  • 在 [Entitlements.plist] 視窗內的 <dict> 與 </dict> 標籤區域內
  • 加入底下宣告
	<key>com.apple.developer.nfc.readersession.formats</key>
	<array>
		<string>NDEF</string>
		<string>TAG</string>
	</array>

上面的操作可以使用底下的做法來取代,會得到同樣的效果

滑鼠雙擊 [Entitlements.plist] 檔案

在 [Entitlements.plist] 視窗內

找到與點選 [近距離無線通訊標籤讀取] 項目

勾選 [啟用近距離無線通訊標籤讀取] 項目

  • 在 iOS 專案 [XFNFC.iOS]
  • 滑鼠右擊 [Info.plist] 檔案,選擇 [開啟方式]
  • 在 [開啟方式 - Info.plist] 對話窗出現後,選擇 [XML (文字) 編輯器]
  • 將底下內容輸入到 </dict> 文字前面
<key>NFCReaderUsageDescription</key>
<string>NFC tag to read NDEF messages into the application</string>
<key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
<string>com.apple.developer.nfc.readersession.iso7816.select-identifiers</string>

讀取 NFC 的操作摘要

Read a tag

  • Start listening with CrossNFC.Current.StartListening().
  • When a NDEF message is received, the event OnMessageReceived is raised.

Write a tag

  • To write a tag, call CrossNFC.Current.StartPublishing()
  • Then CrossNFC.Current.PublishMessage(ITagInfo) when OnTagDiscovered event is raised.
  • Do not forget to call CrossNFC.Current.StopPublishing() once the tag has been written.

Clear a tag

  • To clear a tag, call CrossNFC.Current.StartPublishing(clearMessage: true)
  • Then CrossNFC.Current.PublishMessage(ITagInfo) when OnTagDiscovered event is raised.
  • Do not forget to call CrossNFC.Current.StopPublishing() once the tag has been cleared.

Xamarin.Forms 專案修正 首頁頁面

  • 在 Xamarin.Forms 專案 [XFNFC] 內
  • 打開 MainPage.xaml 檔案
  • 將這個檔案內容替換為底下內容
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XFNFC.Views.MainPage"
             Title="NFC 標籤讀取}">

    <StackLayout HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand">
        <Label Text="Welcome to Xamarin Forms and Prism!" />
        <Label Text="{Binding Tag}"
               FontSize="30" TextColor="Red"/>
        <Button Text="感應 NFC" Command="{Binding ReadNFCCommand}"/>
        <Button Text="停止感應 NFC" Command="{Binding StopReadNFCCommand}"/>
    </StackLayout>

</ContentPage>

修正首頁 ViewModel

  • 打開 MainPageViewModel.cs 檔案
  • 將這個檔案內容替換為底下內容
using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace XFNFC.ViewModels
{
    using System.ComponentModel;
    using System.Threading.Tasks;
    using Plugin.NFC;
    using Prism.Events;
    using Prism.Navigation;
    using Prism.Services;
    using Xamarin.Forms;

    public class MainPageViewModel : INotifyPropertyChanged, INavigationAware
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private readonly INavigationService navigationService;
        public string Tag { get; set; }
        public DelegateCommand ReadNFCCommand { get; set; }
        public DelegateCommand StopReadNFCCommand { get; set; }
        public MainPageViewModel(INavigationService navigationService)
        {
            this.navigationService = navigationService;
            ReadNFCCommand = new DelegateCommand(async () =>
            {
                if (CrossNFC.Current.IsAvailable == false)
                {
                    Tag = "你的裝置沒有 NFC 感應功能";
                    return;
                }
                if (CrossNFC.Current.IsEnabled == false)
                {
                    Tag = "裝置上 NFC 感應功能沒有啟用";
                    return;
                }

                SubscribeEvents();
                StartListen();
            });
            StopReadNFCCommand = new DelegateCommand(() =>
            {
                Tag = "";
                UnsubscribeEvents();
                CrossNFC.Current.StopListening();
            });
        }
        void SubscribeEvents()
        {
            CrossNFC.Current.OnTagDiscovered += OnTagDiscovered;
            CrossNFC.Current.OnMessageReceived += OnMessageReceived;
            CrossNFC.Current.OnMessagePublished += OnMessagePublished;
        }
        void UnsubscribeEvents()
        {
            CrossNFC.Current.OnTagDiscovered -= OnTagDiscovered;
            CrossNFC.Current.OnMessageReceived -= OnMessageReceived;
            CrossNFC.Current.OnMessagePublished -= OnMessagePublished;
        }
        void StartListen()
        {
            try
            {
                CrossNFC.Current.StartListening();
            }
            catch (Exception ex)
            {
                UnsubscribeEvents();
                Console.WriteLine(ex.Message);
            }
        }
        private void OnMessagePublished(ITagInfo tagInfo)
        {
            Tag = tagInfo.SerialNumber;
        }

        private void OnMessageReceived(ITagInfo tagInfo)
        {
            Tag = tagInfo.SerialNumber;
        }

        private void OnTagDiscovered(ITagInfo tagInfo, bool format)
        {
            Tag = tagInfo.SerialNumber;
        }

        public void OnNavigatedFrom(INavigationParameters parameters)
        {
        }

        public void OnNavigatedTo(INavigationParameters parameters)
        {
        }
    }
}




2021年1月8日 星期五

ThreadStatic 與 ThreadLocal 預設值執行結果差異

ThreadStatic 與 ThreadLocal 預設值執行結果差異

對於執行緒本身的儲存空間,也就是 執行緒區域儲存區 Thread Local Storage ,在 .NET 中可以使用 ThreadStaticAttribute 類別 與 ThreadLocal 類別 來做到,前者為 屬性 Attribute 宣告方式,後者為泛型型別;這兩者對於在多執行緒執行下,會有一個明顯的差異,那就是使用前者的話,若有指定預設值,並不會在每個執行緒內第一次取得 ThreadStatic 標示的靜態物件,都會使用宣告的預設值來呈現。

而對於 ThreadLocal 的泛行型別宣告的用法,則是每個執行緒在取用的時候,都會擁有宣告的預設值。

首先,宣告兩個類別,這兩個類別都宣告一個靜態整數成員,分別使用 ThreadStatic 與 ThreadLocal 來定義,而且前者設定預設值為 1 ,而後者宣告預設值為 2

class MyThreadStaticClass
{
    [ThreadStatic]
    public static int MyInt = 1;
}

class MyThreadLocalcClass
{
    public static ThreadLocal<int> MyInt =
        new ThreadLocal<int>(() => 2);
}

現在,首先先來針對 MyThreadStaticClass.MyInt 這個使用 ThreadStatic 宣告的靜態成員來進行測試,在這裡將會使用 Task.Run 產生出 500 個任務,也就是會有 500 執行緒來執行同樣的委派方法,在指定的匿名 Lambda 運算式 方法內,將會顯示出 MyThreadStaticClass.MyInt 這個靜態成員的值。

List<Task> allTasks = new List<Task>();
Console.WriteLine("多執行使用 ThreadStatic 執行結果");
for (int i = 0i < 500i++)
{
    Task task = Task.Run(() =>
    { Console.Write($"{MyThreadStaticClass.MyInt"); });
    allTasks.Add(task);
}
Task.WaitAll(allTasks.ToArray());
Console.WriteLine();

這裡是執行結果,這裡可以看到使用 ThreadStatic 的方式,在多執行緒執行下,並沒有正確地顯示出預期的預設值

多執行使用 ThreadStatic 執行結果
1 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 1 1 0 0 0 0 0 0 0

接著來針對 MyThreadLocalcClass.MyInt 這個使用 ThreadLocal 宣告的靜態成員來進行測試,在這裡將會使用 Task.Run 產生出 500 個任務,也就是會有 500 執行緒來執行同樣的委派方法,在指定的匿名 Lambda 方法內,將會顯示出 MyThreadLocalcClass.MyInt 這個靜態成員的值。

Console.WriteLine("多執行使用 ThreadLocal 執行結果");
allTasks.Clear();
for (int i = 0i < 500i++)
{
    Task task = Task.Run(() =>
    { Console.Write($"{MyThreadLocalcClass.MyInt"); });
    allTasks.Add(task);
}
Task.WaitAll(allTasks.ToArray());
Console.WriteLine();

這裡是執行結果,這裡可以看到使用 ThreadLocal 的方式,在多執行緒執行下,都會有預期的預設值出現

多執行使用 ThreadLocal 執行結果 

2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2