当前位置: 移动技术网 > IT编程>开发语言>c# > Xamarin.Forms客户端第一版

Xamarin.Forms客户端第一版

2020年04月03日  | 移动技术网IT编程  | 我要评论

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
{
    /// <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获取服务功能,功能截图如下:

2.2.1. terminalmacs.clients.app

代码结构如下图:

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

代码结构如下图:

  • 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

代码结构如下图:

  • 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

联系人权限请求说明
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"

具体使用如

<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. 关于项目开源

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网