Maui Blazor 中文社区 QQ群:645660665

bh006- Blazor hybrid / Maui 使用NFC快速教程

1. 建立工程 bh006_NFC_tag

源码 https://github.com/densen2014/BlazorHybrid/tree/master/bh100days/bh006_NFC_tag?WT.mc_id=DT-MVP-5005078

2. 添加 nuget 包

<PackageReference Include="BlazorHybrid.Maui.Permissions" Version="0.0.3" />
<PackageReference Include="BootstrapBlazor" Version="7.*" />
<PackageReference Include="Densen.Extensions.BootstrapBlazor" Version="7.*" />

BlazorHybrid.Maui.Permissions 因为源码比较长,主要是一些检查和申请权限,BLE/NFC权限相关代码,就不占用篇幅列出,感兴趣的同学直接打开源码参考

顺便打开可空 <Nullable>enable</Nullable>

3. 添加蓝牙权限

安卓

Platforms\Android 文件夹

AndroidManifest.xml

  <uses-permission android:name="android.permission.NFC" />
  <uses-feature android:name="android.hardware.nfc" android:required="false" />

添加安卓必要Intent

MainActivity.cs

using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Nfc;
using Android.OS;
using DH.NFC;

namespace bh006_NFC_tag
{
    [Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
    [IntentFilter(new[] { NfcAdapter.ActionNdefDiscovered }, Categories = new[] { Intent.CategoryDefault }, DataMimeType = NfcPage.MIME_TYPE)]
    [IntentFilter(new[] { Platform.Intent.ActionAppAction },
              Categories = new[] { global::Android.Content.Intent.CategoryDefault })]
    public class MainActivity : MauiAppCompatActivity
    {
        protected override void OnCreate(Bundle savedInstanceState)
        {
            // 初始化
            CrossNFC.Init(this);

            base.OnCreate(savedInstanceState);
        }

        protected override void OnResume()
        {
            base.OnResume();

            // 恢复时重新启动NFC监听(Android 10+需要)
            CrossNFC.OnResume();
        }

        protected override void OnNewIntent(Intent intent)
        {
            base.OnNewIntent(intent);

            // 标签发现拦截
            CrossNFC.OnNewIntent(intent);

            //AppActions 
            Platform.OnNewIntent(intent);
        }

    }
}

iOS

Platforms\iOS 文件夹

Info.plist

    <key>NFCReaderUsageDescription</key>
    <string>此应用程序需要NFC标签读取NDEF消息到应用程序中,请根据要求授予权限.</string>
    <key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
    <array>
        <string>com.apple.developer.nfc.readersession.iso7816.select-identifiers</string>
        <string>D2760000850100</string>
    </array>

新建 Entitlements.plist 文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>com.apple.developer.nfc.readersession.formats</key>
        <array>
            <string>NDEF</string>
            <string>TAG</string>
        </array>
    </dict>
</plist>

iOS开发者应用注册清单添加必要权限, (传送门)[]

4. 添加 BootstrapBlazor UI 库请参考前几篇

5. 添加代码后置文件 Pages/Index.razor.cs

Index.razor.cs

using BootstrapBlazor.Components;
using DH.NFC;
using Microsoft.AspNetCore.Components;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Text;

namespace bh006_NFC_tag.Pages;

public partial class Index : IAsyncDisposable
{
    [Inject, NotNull] protected MessageService? MessageService { get; set; }

    [NotNull]
    protected Message? Message { get; set; }

    [DisplayName("使标签为只读")]
    bool ChkReadOnly { get; set; } = false;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // 为了支持Mifare Classic 1K标签(读/写),必须将传统模式设置为true。
            CrossNFC.Legacy = false;

            if (CrossNFC.IsSupported)
            {
                if (!CrossNFC.Current.IsAvailable)
                    await ShowAlert("NFC is not available");

                NfcIsEnabled = CrossNFC.Current.IsEnabled;
                if (!NfcIsEnabled)
                    await ShowAlert("NFC is disabled");

                if (DeviceInfo.Platform == DevicePlatform.iOS)
                    _isDeviceiOS = true;

                await AutoStartAsync().ConfigureAwait(false);
            }

        }
    }

    //连接外设
    private async Task XamlPage()
    {
        MauiProgram.OpenNFCXamlPage();
        //异步更新UI
        await InvokeAsync(StateHasChanged);
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        await StopListening();
    }

    public const string ALERT_TITLE = "NFC";
    public const string MIME_TYPE = "application/com.densen.nfc";

    NFCNdefTypeFormat _type;
    bool _makeReadOnly = false;
    bool _eventsAlreadySubscribed = false;
    bool _isDeviceiOS = false;

    //https://gitee.com/dengho/DH.Maui.FrameWork/blob/master/Demo/DH.NFCDemo/NfcPage.xaml.cs

    /// <summary>
    /// 跟踪Android设备是否仍在监听的属性,
    /// 因此它可以向用户指示这一点。
    /// </summary>
    public bool DeviceIsListening 
    {
        get => _deviceIsListening;
        set
        {
            _deviceIsListening = value;
            StateHasChanged();
        }
    }
    private bool _deviceIsListening;

    private bool _nfcIsEnabled;
    public bool NfcIsEnabled
    {
        get => _nfcIsEnabled;
        set
        {
            _nfcIsEnabled = value;
            StateHasChanged();
        }
    }

    public bool NfcIsDisabled => !NfcIsEnabled;


    /// <summary>
    /// 自动开始收听
    /// </summary>
    /// <returns></returns>
    async Task AutoStartAsync()
    {
        // 在Android上阻止Java.Lang.IllegalStateException的一些延迟“仅当您的活动恢复时才能启用前台调度”
        await Task.Delay(500);
        await StartListeningIfNotiOS();
    }

    /// <summary>
    /// 订阅NFC活动
    /// </summary>
    void SubscribeEvents()
    {
        if (_eventsAlreadySubscribed)
            UnsubscribeEvents();

        _eventsAlreadySubscribed = true;

        CrossNFC.Current.OnMessageReceived += Current_OnMessageReceived;
        CrossNFC.Current.OnMessagePublished += Current_OnMessagePublished;
        CrossNFC.Current.OnTagDiscovered += Current_OnTagDiscovered;
        CrossNFC.Current.OnNfcStatusChanged += Current_OnNfcStatusChanged;
        CrossNFC.Current.OnTagListeningStatusChanged += Current_OnTagListeningStatusChanged;

        if (_isDeviceiOS)
            CrossNFC.Current.OniOSReadingSessionCancelled += Current_OniOSReadingSessionCancelled;
    }

    /// <summary>
    /// 取消订阅NFC活动
    /// </summary>
    void UnsubscribeEvents()
    {
        CrossNFC.Current.OnMessageReceived -= Current_OnMessageReceived;
        CrossNFC.Current.OnMessagePublished -= Current_OnMessagePublished;
        CrossNFC.Current.OnTagDiscovered -= Current_OnTagDiscovered;
        CrossNFC.Current.OnNfcStatusChanged -= Current_OnNfcStatusChanged;
        CrossNFC.Current.OnTagListeningStatusChanged -= Current_OnTagListeningStatusChanged;

        if (_isDeviceiOS)
            CrossNFC.Current.OniOSReadingSessionCancelled -= Current_OniOSReadingSessionCancelled;

        _eventsAlreadySubscribed = false;
    }

    /// <summary>
    /// 侦听器状态更改时引发的事件
    /// </summary>
    /// <param name="isListening"></param>
    void Current_OnTagListeningStatusChanged(bool isListening) => DeviceIsListening = isListening;

    /// <summary>
    /// NFC状态更改时引发的事件
    /// </summary>
    /// <param name="isEnabled">NFC status</param>
    async void Current_OnNfcStatusChanged(bool isEnabled)
    {
        NfcIsEnabled = isEnabled;
        await ShowAlert($"NFC has been {(isEnabled ? "enabled" : "disabled")}");
    }

    /// <summary>
    /// 收到NDEF消息时引发的事件
    /// </summary>
    /// <param name="tagInfo">Received <see cref="ITagInfo"/></param>
    async void Current_OnMessageReceived(ITagInfo tagInfo)
    {
        if (tagInfo == null)
        {
            await ShowAlert("No tag found");
            return;
        }

        // 自定义序列号
        var identifier = tagInfo.Identifier;
        var serialNumber = NFCUtils.ByteArrayToHexString(identifier, ":");
        var title = !string.IsNullOrWhiteSpace(serialNumber) ? $"标签 [{serialNumber}]" : "标签信息";

        if (!tagInfo.IsSupported)
        {
            await ShowAlert("Unsupported tag (app)", title);
        }
        else if (tagInfo.IsEmpty)
        {
            await ShowAlert("Empty tag", title);
        }
        else
        {
            var first = tagInfo.Records[0];
            await ShowAlert(GetMessage(first), title);
        }
    }

    /// <summary>
    /// Event raised when user cancelled NFC session on iOS 
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void Current_OniOSReadingSessionCancelled(object? sender, EventArgs e) => Debug("iOS NFC Session has been cancelled");

    /// <summary>
    /// 在标记上发布数据时引发的事件
    /// </summary>
    /// <param name="tagInfo">发布<see cref="ITagInfo"/></param>
    async void Current_OnMessagePublished(ITagInfo tagInfo)
    {
        try
        {
            ChkReadOnly = false;
            CrossNFC.Current.StopPublishing();
            if (tagInfo.IsEmpty)
                await ShowAlert("Formatting tag operation successful");
            else
                await ShowAlert("Writing tag operation successful");
        }
        catch (Exception ex)
        {
            await ShowAlert(ex.Message);
        }
    }

    /// <summary>
    /// 发现NFC标签时引发的事件
    /// </summary>
    /// <param name="tagInfo"><see cref="ITagInfo"/>被发布</param>
    /// <param name="format">设置标签格式</param>
    async void Current_OnTagDiscovered(ITagInfo tagInfo, bool format)
    {
        if (!CrossNFC.Current.IsWritingTagSupported)
        {
            await ShowAlert("此设备不支持写入标签");
            return;
        }

        try
        {
            NFCNdefRecord? record = null;
            switch (_type)
            {
                case NFCNdefTypeFormat.WellKnown:
                    record = new NFCNdefRecord
                    {
                        TypeFormat = NFCNdefTypeFormat.WellKnown,
                        MimeType = MIME_TYPE,
                        Payload = NFCUtils.EncodeToByteArray("DH.Maui.FrameWork是很棒的!"),
                        LanguageCode = "en"
                    };
                    break;
                case NFCNdefTypeFormat.Uri:
                    record = new NFCNdefRecord
                    {
                        TypeFormat = NFCNdefTypeFormat.Uri,
                        Payload = NFCUtils.EncodeToByteArray("https://gitee.com/dengho/DH.Maui.FrameWork")
                    };
                    break;
                case NFCNdefTypeFormat.Mime:
                    record = new NFCNdefRecord
                    {
                        TypeFormat = NFCNdefTypeFormat.Mime,
                        MimeType = MIME_TYPE,
                        Payload = NFCUtils.EncodeToByteArray("DH.Maui.FrameWork是很棒的!")
                    };
                    break;
                default:
                    break;
            }

            if (!format && record == null)
                throw new Exception("记录不能为空.");

            tagInfo.Records = new[] { record };

            if (format)
                CrossNFC.Current.ClearMessage(tagInfo);
            else
            {
                CrossNFC.Current.PublishMessage(tagInfo, _makeReadOnly);
            }
        }
        catch (Exception ex)
        {
            await ShowAlert(ex.Message);
        }
    }

 
    /// <summary>
    /// 当将引发<see cref="Current_OnTagDiscovered(ITagInfo, bool)"/>事件时,启动发布操作以写入文本标签
    /// </summary>
    async Task Button_Clicked_StartWriting() => await Publish(NFCNdefTypeFormat.WellKnown);

    /// <summary>
    /// 当将引发<see cref="Current_OnTagDiscovered(ITagInfo, bool)"/>事件时,启动发布操作以写入URI标签
    /// </summary>
    async Task Button_Clicked_StartWriting_Uri() => await Publish(NFCNdefTypeFormat.Uri);

    /// <summary>
    /// 当将引发<see cref="Current_OnTagDiscovered(ITagInfo, bool)"/>事件时,启动发布操作以写入自定义标签
    /// </summary>
    async Task Button_Clicked_StartWriting_Custom() => await Publish(NFCNdefTypeFormat.Mime);

    /// <summary>
    /// 当将引发<see cref="Current_OnTagDiscovered(ITagInfo, bool)"/>事件时,启动发布操作以格式化标签
    /// </summary>
    async Task Button_Clicked_FormatTag() => await Publish();

    /// <summary>
    /// 将数据发布到标签的任务
    /// </summary>
    /// <param name="type"><see cref="NFCNdefTypeFormat"/></param>
    /// <returns>要执行的任务</returns>
    async Task Publish(NFCNdefTypeFormat? type = null)
    {
        await StartListeningIfNotiOS();
        try
        {
            _type = NFCNdefTypeFormat.Empty;
            if (ChkReadOnly)
            {
                //if (!await DisplayAlert("警告", "使标签为只读操作是永久性的,无法撤消。您确定要继续吗?", "是", "否"))
                //{
                //    ChkReadOnly = false;
                //    return;
                //}
                _makeReadOnly = true;
            }
            else
                _makeReadOnly = false;

            if (type.HasValue) _type = type.Value;
            CrossNFC.Current.StartPublishing(!type.HasValue);
        }
        catch (Exception ex)
        {
            await ShowAlert(ex.Message);
        }
    }

    /// <summary>
    /// 返回NDEF记录中的标记信息
    /// </summary>
    /// <param name="record"><see cref="NFCNdefRecord"/></param>
    /// <returns>标签信息</returns>
    string GetMessage(NFCNdefRecord record)
    {
        var message = $"消息: {record.Message}";
        message += Environment.NewLine;
        message += $"原始消息: {Encoding.UTF8.GetString(record.Payload)}";
        message += Environment.NewLine;
        message += $"类型: {record.TypeFormat}";

        if (!string.IsNullOrWhiteSpace(record.MimeType))
        {
            message += Environment.NewLine;
            message += $"Mime类型: {record.MimeType}";
        }

        return message;
    }

    /// <summary>
    /// 在调试控制台中编写调试消息
    /// </summary>
    /// <param name="message">要显示的消息</param>
    void Debug(string message) => System.Diagnostics.Debug.WriteLine(message);

    /// <summary>
    /// 显示消息
    /// </summary>
    /// <param name="message">要显示的消息</param>
    /// <param name="title">消息标题</param>
    /// <returns>要执行的任务</returns>
    async Task ShowAlert(string message, string? title = null) =>
    await MessageService.Show(new MessageOption()
    {
        Content = message,
        Icon = "fa-solid fa-circle-info",
    }, Message);

    /// <summary>
    /// 如果用户的设备平台不是iOS,则开始监听NFC标签的任务
    /// </summary>
    /// <returns>要执行的任务</returns>
    async Task StartListeningIfNotiOS()
    {
        if (_isDeviceiOS)
        {
            SubscribeEvents();
            return;
        }
        await BeginListening();
    }

    /// <summary>
    /// 安全开始监听NFC标签的任务
    /// </summary>
    /// <returns>要执行的任务</returns>
    async Task BeginListening()
    {
        try
        {
            MainThread.BeginInvokeOnMainThread(() =>
            {
                SubscribeEvents();
                CrossNFC.Current.StartListening();
            });
        }
        catch (Exception ex)
        {
            await ShowAlert(ex.Message);
        }
    }

    /// <summary>
    /// 安全停止监听NFC标签的任务
    /// </summary>
    /// <returns>要执行的任务</returns>
    async Task StopListening()
    {
        try
        {
            MainThread.BeginInvokeOnMainThread(async () =>
            {
                try
                {

                    CrossNFC.Current.StopListening();
                    UnsubscribeEvents();
                }
                catch (Exception ex)
                {
                    await ShowAlert(ex.Message);
                }
            });
        }
        catch (Exception ex)
        {
            await ShowAlert(ex.Message);
        }
    }


}

6. 添加 UI Pages/Index.razor

Index.razor

@page "/"

<h3>NFC</h3>

<div class="row g-3">

    <Button OnClick="BeginListening"
            IsDisabled="NfcIsDisabled"
            Text="读取标签" />

    <Button OnClick="StopListening"
            IsDisabled="NfcIsDisabled"
            Text="停止侦听器" />

    <GroupBox Title="写入">

        @*<CheckBox TValue="bool" DisplayText="双向绑定" ShowLabel="true" @bind-value="@ChkReadOnly" IsDisabled="NfcIsDisabled" />*@

        <Button OnClick="Button_Clicked_StartWriting"
                IsDisabled="NfcIsDisabled"
                Text="标签写入文本" />

        <Button OnClick="Button_Clicked_StartWriting_Uri"
                IsDisabled="NfcIsDisabled"
                Text="标签写入网址" />

        <Button OnClick="Button_Clicked_StartWriting_Custom"
                IsDisabled="NfcIsDisabled"
                Text="标签写入自定义内容" />

    </GroupBox>

    <Button OnClick="Button_Clicked_FormatTag"
            IsDisabled="NfcIsDisabled"
            Text="标签清除" />

    @if (DeviceIsListening)
    {
        <p>监听NFC标签...</p>

    }

    @if (NfcIsDisabled)
    {
        <p>NFC已禁用</p>

    }

    <div class="btn-group" role="group">
        <Button Text="NFC XAML" OnClick=XamlPage />
    </div>

</div>

<pre style="max-height: 500px; overflow-y: scroll; white-space: pre-wrap; word-wrap: break-word;">@Message</pre>

10. 运行

11. 相关资料

如何远程调试 MAUI blazor / Blazor Hybrid
https://www.cnblogs.com/densen2014/p/16988516.html

posted @ 2023-08-25 19:24  AlexChow  阅读(401)  评论(0编辑  收藏  举报