欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Xamarin.Forms客户端第一版

程序员文章站 2022-03-12 18:47:04
Xamarin.Forms客户端第一版 作为TerminalMACS的一个子进程模块,目前完成第一版:读取展示手机基本信息、联系人信息、应用程序本地化。 1. 功能简介 2. 详细功能说明 3. 关于TerminalMACS 1. 功能简介 1.1. 读取手机基本信息 主要使用Xamarin.Ess ......

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,用于唯一标识不同手机,获取信息见下图:

Xamarin.Forms客户端第一版

代码结构如下图:

Xamarin.Forms客户端第一版

clientinfoviewmodel.cs

using plugin.deviceinfo;
using system;
using xamarin.essentials;

namespace terminalmacs.clients.app.viewmodels
{
    /// <summary>
    /// client base information page viewmodel
    /// </summary>
    public class clientinfoviewmodel : baseviewmodel
    {
        /// <summary>
        /// gets or sets the id of the application.
        /// </summary>
        public string appid { get; set; } = crossdeviceinfo.current.generateappid();
        /// <summary>
        /// gets or sets the model of the device.
        /// </summary>
        public string model { get; private set; } = deviceinfo.model;
        /// <summary>
        /// gets or sets the manufacturer of the device.
        /// </summary>
        public string manufacturer { get; private set; } = deviceinfo.manufacturer;
        /// <summary>
        /// gets or sets the name of the device.
        /// </summary>
        public string name { get; private set; } = deviceinfo.name;
        /// <summary>
        /// gets or sets the version of the operating system.
        /// </summary>
        public string versionstring { get; private set; } = deviceinfo.versionstring;
        /// <summary>
        /// gets or sets the version of the operating system.
        /// </summary>
        public version version { get; private set; } = deviceinfo.version;
        /// <summary>
        /// gets or sets the platform or operating system of the device.
        /// </summary>
        public deviceplatform platform { get; private set; } = deviceinfo.platform;
        /// <summary>
        /// gets or sets the idiom of the device.
        /// </summary>
        public deviceidiom idiom { get; private set; } = deviceinfo.idiom;
        /// <summary>
        /// gets or sets the type of device the application is running on.
        /// </summary>
        public devicetype devicetype { get; private set; } = deviceinfo.devicetype;
    }
}

clientinfopage.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"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://xamarin.com/schemas/2014/forms/design"
             xmlns:resources="clr-namespace:terminalmacs.clients.app.resx"
             xmlns:vm="clr-namespace:terminalmacs.clients.app.viewmodels"
             mc:ignorable="d"
             x:class="terminalmacs.clients.app.views.clientinfopage"
             title="{x:static resources:appresource.title_clientinfopage}">
    <contentpage.bindingcontext>
        <vm:clientinfoviewmodel/>
    </contentpage.bindingcontext>
    <contentpage.content>
        <stacklayout>
            <label text="{x:static resources:appresource.appid}"/>
            <label text="{binding appid}" fontattributes="bold" margin="10,0,0,10"/>
            
            <label text="{x:static resources:appresource.devicemodel}"/>
            <label text="{binding model}" fontattributes="bold" margin="10,0,0,10"/>

            <label text="{x:static resources:appresource.devicemanufacturer}"/>
            <label text="{binding manufacturer}" fontattributes="bold" margin="10,0,0,10"/>
            
            <label text="{x:static resources:appresource.devicename}"/>
            <label text="{binding name}" fontattributes="bold" margin="10,0,0,10"/>
            
            <label text="{x:static resources:appresource.deviceversionstring}"/>
            <label text="{binding versionstring}" fontattributes="bold" margin="10,0,0,10"/>

            <label text="{x:static resources:appresource.deviceplatform}"/>
            <label text="{binding platform}" fontattributes="bold" margin="10,0,0,10"/>
            
            <label text="{x:static resources:appresource.deviceidiom}"/>
            <label text="{binding idiom}" fontattributes="bold" margin="10,0,0,10"/>
            
            <label text="{x:static resources:appresource.devicetype}"/>
            <label text="{binding devicetype}" fontattributes="bold" margin="10,0,0,10"/>
        </stacklayout>
    </contentpage.content>
</contentpage>

2.2. 读取手机联系人信息

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

Xamarin.Forms客户端第一版

2.2.1. terminalmacs.clients.app

代码结构如下图:

Xamarin.Forms客户端第一版

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

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

namespace terminalmacs.clients.app.models
{
    /// <summary>
    /// contact information entity.
    /// </summary>
    public class contact
    {
        /// <summary>
        /// gets or sets the name
        /// </summary>
        public string name { get; set; }
        /// <summary>
        /// gets or sets the image
        /// </summary>
        public string image { get; set; }
        /// <summary>
        /// gets or sets the emails
        /// </summary>
        public string[] emails { get; set; }
        /// <summary>
        /// gets or sets the phone numbers
        /// </summary>
        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
{
    /// <summary>
    /// read a contact record notification event parameter.
    /// </summary>
    public class contacteventargs:eventargs
    {
        public contact contact { get; }
        public contacteventargs(contact contact)
        {
            contact = contact;
        }
    }

    /// <summary>
    /// contact service interface, which is required for android and ios terminal specific 
    ///  contact acquisition service needs to implement this interface.
    /// </summary>
    public interface icontactsservice
    {
        /// <summary>
        /// read a contact record and notify the shared library through this event.
        /// </summary>
        event eventhandler<contacteventargs> oncontactloaded;
        /// <summary>
        /// loading or not
        /// </summary>
        bool isloading { get; }
        /// <summary>
        /// try to get all contact information
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        task<ilist<contact>> 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
{
    /// <summary>
    /// contact page viewmodel
    /// </summary>
    public class contactviewmodel : baseviewmodel
    {
        /// <summary>
        /// contact service interface
        /// </summary>
        icontactsservice _contactservice;
        private string _searchtext;
        /// <summary>
        /// gets or sets the search text of the contact list.
        /// </summary>
        public string searchtext
        {
            get { return _searchtext; }
            set
            {
                setproperty(ref _searchtext, value);
            }
        }
        /// <summary>
        /// the search contact command.
        /// </summary>
        public icommand raisesearchcommand { get; }
        /// <summary>
        /// the contact list.
        /// </summary>
        public observablecollection<contact> contacts { get; set; }
        private list<contact> _filteredcontacts;
        /// <summary>
        /// contact filter list.
        /// </summary>
        public list<contact> filteredcontacts

        {
            get { return _filteredcontacts; }
            set
            {
                setproperty(ref _filteredcontacts, value);
            }
        }
        public contactviewmodel()
        {
            _contactservice = dependencyservice.get<icontactsservice>();
            contacts = new observablecollection<contact>();
            xamarin.forms.bindingbase.enablecollectionsynchronization(contacts, null, observablecollectioncallback);
            _contactservice.oncontactloaded += oncontactloaded;
            loadcontacts();
            raisesearchcommand = new command(raisesearchhandle);
        }

        /// <summary>
        /// filter contact list
        /// </summary>
        void raisesearchhandle()
        {
            if (string.isnullorempty(searchtext))
            {
                filteredcontacts = contacts.tolist();
                return;
            }

            func<contact, bool> 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();
        }

        /// <summary>
        /// bindingbase.enablecollectionsynchronization
        ///     enable cross thread updates for collections
        /// </summary>
        /// <param name="collection"></param>
        /// <param name="context"></param>
        /// <param name="accessmethod"></param>
        /// <param name="writeaccess"></param>
        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();
            }
        }

        /// <summary>
        /// received a event notification that a contact information was successfully read.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void oncontactloaded(object sender, contacteventargs e)
        {
            contacts.add(e.contact);
            raisesearchhandle();
        }

        /// <summary>
        /// read contact information asynchronously
        /// </summary>
        /// <returns></returns>
        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" ?>
<contentpage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://xamarin.com/schemas/2014/forms/design"
             xmlns:resources="clr-namespace:terminalmacs.clients.app.resx"
             xmlns:vm="clr-namespace:terminalmacs.clients.app.viewmodels"
             xmlns:ios="clr-namespace:xamarin.forms.platformconfiguration.iosspecific;assembly=xamarin.forms.core"
             mc:ignorable="d"
             title="{x:static resources:appresource.title_contactpage}"
             x:class="terminalmacs.clients.app.views.contactpage"
             ios:page.usesafearea="true">
    <contentpage.bindingcontext>
        <vm:contactviewmodel/>
    </contentpage.bindingcontext>
    <contentpage.content>
        <stacklayout>
            <searchbar x:name="filtertext"
                        heightrequest="40"
                        text="{binding searchtext}"
                       searchcommand="{binding raisesearchcommand}"/>
            <listview   itemssource="{binding filteredcontacts}"
                        hasunevenrows="true">
                <listview.itemtemplate>
                    <datatemplate>
                        <viewcell>
                            <stacklayout padding="10"
                                         orientation="horizontal">
                                <image  source="{binding image}"
                                        verticaloptions="center"
                                        x:name="image"
                                        aspect="aspectfit"
                                        heightrequest="60"/>
                                <stacklayout verticaloptions="center">
                                    <label text="{binding name}"
                                       fontattributes="bold"/>
                                    <label text="{binding phonenumbers[0]}"/>
                                    <label text="{binding emails[0]}"/>
                                </stacklayout>
                            </stacklayout>
                        </viewcell>
                    </datatemplate>
                </listview.itemtemplate>
            </listview>
        </stacklayout>
    </contentpage.content>
</contentpage>

2.2.2. android

代码结构如下图:

Xamarin.Forms客户端第一版

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

2.2.2.1. androidmanifest.xml添加权限

只添加下面这一行即可:

<uses-permission android:name="android.permission.read_contacts" />

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
{
    /// <summary>
    /// contact service.
    /// </summary>
    public class contactsservice : nsobject, icontactsservice
    {
        const string thumbnailprefix = "thumb";

        bool requeststop = false;

        public event eventhandler<contacteventargs> oncontactloaded;

        bool _isloading = false;
        public bool isloading => _isloading;

        /// <summary>
        /// asynchronous request permission
        /// </summary>
        /// <returns></returns>
        public async task<bool> requestpermissionasync()
        {
            var status = cncontactstore.getauthorizationstatus(cnentitytype.contacts);

            tuple<bool, nserror> authotization = new tuple<bool, nserror>(status == cnauthorizationstatus.authorized, null);

            if (status == cnauthorizationstatus.notdetermined)
            {
                using (var store = new cncontactstore())
                {
                    authotization = await store.requestaccessasync(cnentitytype.contacts);
                }
            }
            return authotization.item1;

        }

        /// <summary>
        /// request contact asynchronously. this method is called by the interface.
        /// </summary>
        /// <param name="canceltoken"></param>
        /// <returns></returns>
        public async task<ilist<contact>> retrievecontactsasync(cancellationtoken? canceltoken = null)
        {
            requeststop = false;

            if (!canceltoken.hasvalue)
                canceltoken = cancellationtoken.none;

            // we create a taskcompletionsource of decimal
            var taskcompletionsource = new taskcompletionsource<ilist<contact>>();

            // 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;

        }

        /// <summary>
        /// load contacts asynchronously, fact reading method of address book.
        /// </summary>
        /// <returns></returns>
        async task<ilist<contact>> loadcontactsasync()
        {
            ilist<contact> contacts = new list<contact>();
            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

代码结构如下图:

Xamarin.Forms客户端第一版

  • 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
{
    /// <summary>
    /// contact service.
    /// </summary>
    public class contactsservice : nsobject, icontactsservice
    {
        const string thumbnailprefix = "thumb";

        bool requeststop = false;

        public event eventhandler<contacteventargs> oncontactloaded;

        bool _isloading = false;
        public bool isloading => _isloading;

        /// <summary>
        /// asynchronous request permission
        /// </summary>
        /// <returns></returns>
        public async task<bool> requestpermissionasync()
        {
            var status = cncontactstore.getauthorizationstatus(cnentitytype.contacts);

            tuple<bool, nserror> authotization = new tuple<bool, nserror>(status == cnauthorizationstatus.authorized, null);

            if (status == cnauthorizationstatus.notdetermined)
            {
                using (var store = new cncontactstore())
                {
                    authotization = await store.requestaccessasync(cnentitytype.contacts);
                }
            }
            return authotization.item1;

        }

        /// <summary>
        /// request contact asynchronously. this method is called by the interface.
        /// </summary>
        /// <param name="canceltoken"></param>
        /// <returns></returns>
        public async task<ilist<contact>> retrievecontactsasync(cancellationtoken? canceltoken = null)
        {
            requeststop = false;

            if (!canceltoken.hasvalue)
                canceltoken = cancellationtoken.none;

            // we create a taskcompletionsource of decimal
            var taskcompletionsource = new taskcompletionsource<ilist<contact>>();

            // 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;

        }

        /// <summary>
        /// load contacts asynchronously, fact reading method of address book.
        /// </summary>
        /// <returns></returns>
        async task<ilist<contact>> loadcontactsasync()
        {
            ilist<contact> contacts = new list<contact>();
            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

联系*限请求说明
Xamarin.Forms客户端第一版

2.3. 应用本地化

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

Xamarin.Forms客户端第一版

资源文件如下:

Xamarin.Forms客户端第一版

指定默认区域性

要使资源文件可正常使用,应用程序必须指定 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"

具体使用如

<label text="{x:static resources:appresource.clientname_aboutpage}" fontattributes="bold"/>

3. 关于terminalmacs及本客户端

3.1. termainmacs

多终端资源管理与检测系统,包含多个子进程模块,目前只开发了xamarin.forms客户端,下一步开发服务端,使用 .net 5 web api开发,基于abp vnext搭建。

3.2. xamarin.forms客户端

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

3.3. 关于项目开源