Xamarin.Forms客户端第一版


Xamarin.Forms客户端第一版

作为TerminalMACS的一个子进程模块,目前完成第一版:读取展示手机基本信息、联系人信息、应用程序本地化。

  1. 功能简介
  2. 详细功能说明
  3. 关于TerminalMACS

1. 功能简介

1.1. 读取手机基本信息

主要使用Xamarin.Essentials库获取设备基本信息,Xam.Plugin.DeviceInfo插件获取App Id,其实该插件也能获取设备基本信息。

1.2. 读取手机联系人信息

Android和iOS工程具体实现联系人读取服务,使用到DependencyService获取服务功能。

1.3. 应用本地化

使用资源文件实现本地化,目前只做了中、英文。

2. 详细功能说明

2.1. 读取手机基本信息

Xamarin.Essentials库用于获取手机基本信息,比如手机厂商、型号、名称、类型、版本等;Xam.Plugin.DeviceInfo插件获取App Id,用于唯一标识不同手机,获取信息见下图:

代码结构如下图:

ClientInfoViewModel.cs

using Plugin.DeviceInfo;
using System;
using Xamarin.Essentials;

namespace TerminalMACS.Clients.App.ViewModels
{
    /// 
    /// Client base information page ViewModel
    /// 
    public class ClientInfoViewModel : BaseViewModel
    {
        /// 
        /// Gets or sets the id of the application.
        /// 
        public string AppId { get; set; } = CrossDeviceInfo.Current.GenerateAppId();
        /// 
        /// Gets or sets the model of the device.
        /// 
        public string Model { get; private set; } = DeviceInfo.Model;
        /// 
        /// Gets or sets the manufacturer of the device.
        /// 
        public string Manufacturer { get; private set; } = DeviceInfo.Manufacturer;
        /// 
        /// Gets or sets the name of the device.
        /// 
        public string Name { get; private set; } = DeviceInfo.Name;
        /// 
        /// Gets or sets the version of the operating system.
        /// 
        public string VersionString { get; private set; } = DeviceInfo.VersionString;
        /// 
        /// Gets or sets the version of the operating system.
        /// 
        public Version Version { get; private set; } = DeviceInfo.Version;
        /// 
        /// Gets or sets the platform or operating system of the device.
        /// 
        public DevicePlatform Platform { get; private set; } = DeviceInfo.Platform;
        /// 
        /// Gets or sets the idiom of the device.
        /// 
        public DeviceIdiom Idiom { get; private set; } = DeviceInfo.Idiom;
        /// 
        /// Gets or sets the type of device the application is running on.
        /// 
        public DeviceType DeviceType { get; private set; } = DeviceInfo.DeviceType;
    }
}

ClientInfoPage.xaml

<?xml version="1.0" encoding="utf-8" ?>

    
        
    
    
        
            
    

2.2. 读取手机联系人信息

Android和iOS工程具体实现联系人读取服务,使用到DependencyService获取服务功能,功能截图如下:

2.2.1. TerminalMACS.Clients.App

代码结构如下图:

2.2.1.1. 联系人实体类:Contacts.cs

目前只获取联系人名称、图片、电子邮件(可能多个)、电话号码(可能多个),更多可以扩展。

namespace TerminalMACS.Clients.App.Models
{
    /// 
    /// Contact information entity.
    /// 
    public class Contact
    {
        /// 
        /// Gets or sets the name
        /// 
        public string Name { get; set; }
        /// 
        /// Gets or sets the image
        /// 
        public string Image { get; set; }
        /// 
        /// Gets or sets the emails
        /// 
        public string[] Emails { get; set; }
        /// 
        /// Gets or sets the phone numbers
        /// 
        public string[] PhoneNumbers { get; set; }
    }
}
2.2.1.2. 联系人服务接口:IContactsService.cs

包括:

  • 一个联系人获取请求接口:RetrieveContactsAsync
  • 一个读取一条联系人结果通知事件:OnContactLoaded

该接口由具体平台(Android和iOS)实现。

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using TerminalMACS.Clients.App.Models;

namespace TerminalMACS.Clients.App.Services
{
    /// 
    /// Read a contact record notification event parameter.
    /// 
    public class ContactEventArgs:EventArgs
    {
        public Contact Contact { get; }
        public ContactEventArgs(Contact contact)
        {
            Contact = contact;
        }
    }

    /// 
    /// Contact service interface, which is required for Android and iOS terminal specific 
    ///  contact acquisition service needs to implement this interface.
    /// 
    public interface IContactsService
    {
        /// 
        /// Read a contact record and notify the shared library through this event.
        /// 
        event EventHandler OnContactLoaded;
        /// 
        /// Loading or not
        /// 
        bool IsLoading { get; }
        /// 
        /// Try to get all contact information
        /// 
        /// 
        /// 
        Task> RetrieveContactsAsync(CancellationToken? token = null);
    }
}
2.2.1.3. 联系人VM:ContactViewModel.cs

VM提供下面两个功能:

  1. 全部联系人加载。
  2. 联系人关键字查询。
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using TerminalMACS.Clients.App.Models;
using TerminalMACS.Clients.App.Resx;
using TerminalMACS.Clients.App.Services;
using Xamarin.Forms;

namespace TerminalMACS.Clients.App.ViewModels
{
    /// 
    /// Contact page ViewModel
    /// 
    public class ContactViewModel : BaseViewModel
    {
        /// 
        /// Contact service interface
        /// 
        IContactsService _contactService;
        private string _SearchText;
        /// 
        /// Gets or sets the search text of the contact list.
        /// 
        public string SearchText
        {
            get { return _SearchText; }
            set
            {
                SetProperty(ref _SearchText, value);
            }
        }
        /// 
        /// The search contact command.
        /// 
        public ICommand RaiseSearchCommand { get; }
        /// 
        /// The contact list.
        /// 
        public ObservableCollection Contacts { get; set; }
        private List _FilteredContacts;
        /// 
        /// Contact filter list.
        /// 
        public List FilteredContacts

        {
            get { return _FilteredContacts; }
            set
            {
                SetProperty(ref _FilteredContacts, value);
            }
        }
        public ContactViewModel()
        {
            _contactService = DependencyService.Get();
            Contacts = new ObservableCollection();
            Xamarin.Forms.BindingBase.EnableCollectionSynchronization(Contacts, null, ObservableCollectionCallback);
            _contactService.OnContactLoaded += OnContactLoaded;
            LoadContacts();
            RaiseSearchCommand = new Command(RaiseSearchHandle);
        }

        /// 
        /// Filter contact list
        /// 
        void RaiseSearchHandle()
        {
            if (string.IsNullOrEmpty(SearchText))
            {
                FilteredContacts = Contacts.ToList();
                return;
            }

            Func checkContact = (s) =>
            {
                if (!string.IsNullOrWhiteSpace(s.Name) && s.Name.ToLower().Contains(SearchText.ToLower()))
                {
                    return true;
                }
                else if (s.PhoneNumbers.Length > 0 && s.PhoneNumbers.ToList().Exists(cu => cu.ToString().Contains(SearchText)))
                {
                    return true;
                }
                return false;
            };
            FilteredContacts = Contacts.ToList().Where(checkContact).ToList();
        }

        /// 
        /// BindingBase.EnableCollectionSynchronization
        ///     Enable cross thread updates for collections
        /// 
        /// 
        /// 
        /// 
        /// 
        void ObservableCollectionCallback(IEnumerable collection, object context, Action accessMethod, bool writeAccess)
        {
            // `lock` ensures that only one thread access the collection at a time
            lock (collection)
            {
                accessMethod?.Invoke();
            }
        }

        /// 
        /// Received a event notification that a contact information was successfully read.
        /// 
        /// 
        /// 
        private void OnContactLoaded(object sender, ContactEventArgs e)
        {
            Contacts.Add(e.Contact);
            RaiseSearchHandle();
        }

        /// 
        /// Read contact information asynchronously
        /// 
        /// 
        async Task LoadContacts()
        {
            try
            {
                await _contactService.RetrieveContactsAsync();
            }
            catch (TaskCanceledException)
            {
                Console.WriteLine(AppResource.TaskCancelled);
            }
        }
    }
}
2.2.1.4. 联系人展示页面:ContactPage.xaml

简单的布局,一个StackLayout布局容器竖直排列,一个SearchBar提供关键字搜索功能。

<?xml version="1.0" encoding="utf-8" ?>

    
        
    
    
        
            
            
                
                    
                        
                            
                                
                                
                                    
                            
                        
                    
                
            
        
    

2.2.2. Android

代码结构如下图:

  • AndroidManifest.xml:写入读、写联系人权限请求。
  • ContactsService.cs:具体的联系人权限请求、数据读取操作。
  • MainActivity.cs:接收权限请求结果
  • MainApplicaion.cs:此类未添加任务关键代码,但必不可少,否则无法正确弹出权限请求窗口。
  • PermissionUtil.cs:权限请求结果判断

2.2.2.1. AndroidManifest.xml添加权限

只添加下面这一行即可:


2.2.2.2. ContactsService.cs

Android联系人获取实现服务,实现IContactsService。注意命名空间上的特性代码,必须添加上这个特性后,在前面的联系人VM中才能使用DependencyService.Get()获取此服务实例,默认服务是单例的:

[assembly: Xamarin.Forms.Dependency(typeof(TerminalMACS.Clients.App.iOS.Services.ContactsService))]
using Contacts;
using Foundation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using TerminalMACS.Clients.App.Models;
using TerminalMACS.Clients.App.Services;

[assembly: Xamarin.Forms.Dependency(typeof(TerminalMACS.Clients.App.iOS.Services.ContactsService))]
namespace TerminalMACS.Clients.App.iOS.Services
{
    /// 
    /// Contact service.
    /// 
    public class ContactsService : NSObject, IContactsService
    {
        const string ThumbnailPrefix = "thumb";

        bool requestStop = false;

        public event EventHandler OnContactLoaded;

        bool _isLoading = false;
        public bool IsLoading => _isLoading;

        /// 
        /// Asynchronous request permission
        /// 
        /// 
        public async Task RequestPermissionAsync()
        {
            var status = CNContactStore.GetAuthorizationStatus(CNEntityType.Contacts);

            Tuple authotization = new Tuple(status == CNAuthorizationStatus.Authorized, null);

            if (status == CNAuthorizationStatus.NotDetermined)
            {
                using (var store = new CNContactStore())
                {
                    authotization = await store.RequestAccessAsync(CNEntityType.Contacts);
                }
            }
            return authotization.Item1;

        }

        /// 
        /// Request contact asynchronously. This method is called by the interface.
        /// 
        /// 
        /// 
        public async Task> RetrieveContactsAsync(CancellationToken? cancelToken = null)
        {
            requestStop = false;

            if (!cancelToken.HasValue)
                cancelToken = CancellationToken.None;

            // We create a TaskCompletionSource of decimal
            var taskCompletionSource = new TaskCompletionSource>();

            // Registering a lambda into the cancellationToken
            cancelToken.Value.Register(() =>
            {
                // We received a cancellation message, cancel the TaskCompletionSource.Task
                requestStop = true;
                taskCompletionSource.TrySetCanceled();
            });

            _isLoading = true;

            var task = LoadContactsAsync();

            // Wait for the first task to finish among the two
            var completedTask = await Task.WhenAny(task, taskCompletionSource.Task);
            _isLoading = false;

            return await completedTask;

        }

        /// 
        /// Load contacts asynchronously, fact reading method of address book.
        /// 
        /// 
        async Task> LoadContactsAsync()
        {
            IList contacts = new List();
            var hasPermission = await RequestPermissionAsync();
            if (hasPermission)
            {

                NSError error = null;
                var keysToFetch = new[] { CNContactKey.PhoneNumbers, CNContactKey.GivenName, CNContactKey.FamilyName, CNContactKey.EmailAddresses, CNContactKey.ImageDataAvailable, CNContactKey.ThumbnailImageData };

                var request = new CNContactFetchRequest(keysToFetch: keysToFetch);
                request.SortOrder = CNContactSortOrder.GivenName;

                using (var store = new CNContactStore())
                {
                    var result = store.EnumerateContacts(request, out error, new CNContactStoreListContactsHandler((CNContact c, ref bool stop) =>
                    {

                        string path = null;
                        if (c.ImageDataAvailable)
                        {
                            path = path = Path.Combine(Path.GetTempPath(), $"{ThumbnailPrefix}-{Guid.NewGuid()}");

                            if (!File.Exists(path))
                            {
                                var imageData = c.ThumbnailImageData;
                                imageData?.Save(path, true);


                            }
                        }

                        var contact = new Contact()
                        {
                            Name = string.IsNullOrEmpty(c.FamilyName) ? c.GivenName : $"{c.GivenName} {c.FamilyName}",
                            Image = path,
                            PhoneNumbers = c.PhoneNumbers?.Select(p => p?.Value?.StringValue).ToArray(),
                            Emails = c.EmailAddresses?.Select(p => p?.Value?.ToString()).ToArray(),

                        };

                        if (!string.IsNullOrWhiteSpace(contact.Name))
                        {
                            OnContactLoaded?.Invoke(this, new ContactEventArgs(contact));

                            contacts.Add(contact);
                        }

                        stop = requestStop;

                    }));
                }
            }

            return contacts;
        }


    }
}

2.2.2.3. MainActivity.cs

代码简单,只在OnRequestPermissionsResult方法中接收权限请求结果:

// The contact service processes the result of the permission request.
ContactsService.OnRequestPermissionsResult(requestCode, permissions, grantResults);

2.2.3. iOS

代码结构如下图:

  • ContactsService.cs:具体的联系人权限请求、数据读取操作。
  • Info.plist:权限请求时描述文件

2.2.3.1. ContactsService.cs

iOS具体的联系人读取服务,实现IContactsService接口,原理同Android联系人服务类似,本人无调试环境,iOS此功能未测试。

using Contacts;
using Foundation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using TerminalMACS.Clients.App.Models;
using TerminalMACS.Clients.App.Services;

[assembly: Xamarin.Forms.Dependency(typeof(TerminalMACS.Clients.App.iOS.Services.ContactsService))]
namespace TerminalMACS.Clients.App.iOS.Services
{
    /// 
    /// Contact service.
    /// 
    public class ContactsService : NSObject, IContactsService
    {
        const string ThumbnailPrefix = "thumb";

        bool requestStop = false;

        public event EventHandler OnContactLoaded;

        bool _isLoading = false;
        public bool IsLoading => _isLoading;

        /// 
        /// Asynchronous request permission
        /// 
        /// 
        public async Task RequestPermissionAsync()
        {
            var status = CNContactStore.GetAuthorizationStatus(CNEntityType.Contacts);

            Tuple authotization = new Tuple(status == CNAuthorizationStatus.Authorized, null);

            if (status == CNAuthorizationStatus.NotDetermined)
            {
                using (var store = new CNContactStore())
                {
                    authotization = await store.RequestAccessAsync(CNEntityType.Contacts);
                }
            }
            return authotization.Item1;

        }

        /// 
        /// Request contact asynchronously. This method is called by the interface.
        /// 
        /// 
        /// 
        public async Task> RetrieveContactsAsync(CancellationToken? cancelToken = null)
        {
            requestStop = false;

            if (!cancelToken.HasValue)
                cancelToken = CancellationToken.None;

            // We create a TaskCompletionSource of decimal
            var taskCompletionSource = new TaskCompletionSource>();

            // Registering a lambda into the cancellationToken
            cancelToken.Value.Register(() =>
            {
                // We received a cancellation message, cancel the TaskCompletionSource.Task
                requestStop = true;
                taskCompletionSource.TrySetCanceled();
            });

            _isLoading = true;

            var task = LoadContactsAsync();

            // Wait for the first task to finish among the two
            var completedTask = await Task.WhenAny(task, taskCompletionSource.Task);
            _isLoading = false;

            return await completedTask;

        }

        /// 
        /// Load contacts asynchronously, fact reading method of address book.
        /// 
        /// 
        async Task> LoadContactsAsync()
        {
            IList contacts = new List();
            var hasPermission = await RequestPermissionAsync();
            if (hasPermission)
            {

                NSError error = null;
                var keysToFetch = new[] { CNContactKey.PhoneNumbers, CNContactKey.GivenName, CNContactKey.FamilyName, CNContactKey.EmailAddresses, CNContactKey.ImageDataAvailable, CNContactKey.ThumbnailImageData };

                var request = new CNContactFetchRequest(keysToFetch: keysToFetch);
                request.SortOrder = CNContactSortOrder.GivenName;

                using (var store = new CNContactStore())
                {
                    var result = store.EnumerateContacts(request, out error, new CNContactStoreListContactsHandler((CNContact c, ref bool stop) =>
                    {

                        string path = null;
                        if (c.ImageDataAvailable)
                        {
                            path = path = Path.Combine(Path.GetTempPath(), $"{ThumbnailPrefix}-{Guid.NewGuid()}");

                            if (!File.Exists(path))
                            {
                                var imageData = c.ThumbnailImageData;
                                imageData?.Save(path, true);


                            }
                        }

                        var contact = new Contact()
                        {
                            Name = string.IsNullOrEmpty(c.FamilyName) ? c.GivenName : $"{c.GivenName} {c.FamilyName}",
                            Image = path,
                            PhoneNumbers = c.PhoneNumbers?.Select(p => p?.Value?.StringValue).ToArray(),
                            Emails = c.EmailAddresses?.Select(p => p?.Value?.ToString()).ToArray(),

                        };

                        if (!string.IsNullOrWhiteSpace(contact.Name))
                        {
                            OnContactLoaded?.Invoke(this, new ContactEventArgs(contact));

                            contacts.Add(contact);
                        }

                        stop = requestStop;

                    }));
                }
            }

            return contacts;
        }
    }
}

2.2.3.2. Info.plist

联系人权限请求说明
Info.plist

2.3. 应用本地化

使用资源文件实现本地化,目前只做了中、英文。

资源文件如下:

指定默认区域性

要使资源文件可正常使用,应用程序必须指定 NeutralResourcesLanguage。 在共享项目中,应自定义 AssemblyInfo.cs 文件以指定默认区域性 。 以下代码演示如何在 AssemblyInfo.cs 文件中将 NeutralResourcesLanguage 设置为 zh-CN (摘自官方文档:https://docs.microsoft.com/zh-cn/samples/xamarin/xamarin-forms-samples/usingresxlocalization/,后经测试,注释下面这段代码也能正常本地化):

[assembly: NeutralResourcesLanguage("zh-CN")]

XAML中使用

引入资源文件命名空间

xmlns:resources="clr-namespace:TerminalMACS.Clients.App.Resx"

具体使用如

3. 关于TerminalMACS及本客户端

3.1. TermainMACS

多终端资源管理与检测系统,包含多个子进程模块,目前只开发了Xamarin.Forms客户端,下一步开发服务端,使用 .NET 5 Web API开发,基于Abp vNext搭建。

3.2. Xamarin.Forms客户端

作为TerminalMACS系统的一个子进程模块,目前只开发了手机基本信息获取、联系人信息获取、本地化功能,后续开发服务端时,会配合添加通信功能,比如连接服务端验证、主动推送已获取资源等。

3.3. 关于项目开源

    1. 开源项目地址:https://github.com/dotnet9/TerminalMACS
    1. 官方网站:https://terminalmacs.com
    1. 合作网站:https://dotnet9.com