Xamarin.Forms客户端第一版
xamarin.forms客户端第一版
作为terminalmacs的一个子进程模块,目前完成第一版:读取展示手机基本信息、联系人信息、应用程序本地化。
- 功能简介
- 详细功能说明
- 关于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提供下面两个功能:
- 全部联系人加载。
- 联系人关键字查询。
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
代码简单,只在onrequestpermissionsresult方法中接收权限请求结果: 代码结构如下图:
ios具体的联系人读取服务,实现icontactsservice接口,原理同android联系人服务类似,本人无调试环境,ios此功能未测试。 联系*限请求说明 使用资源文件实现本地化,目前只做了中、英文。
资源文件如下:
指定默认区域性 要使资源文件可正常使用,应用程序必须指定 neutralresourceslanguage。 在共享项目中,应自定义 assemblyinfo.cs 文件以指定默认区域性 。 以下代码演示如何在 assemblyinfo.cs 文件中将 neutralresourceslanguage 设置为 zh-cn (摘自官方文档:https://docs.microsoft.com/zh-cn/samples/xamarin/xamarin-forms-samples/usingresxlocalization/,后经测试,注释下面这段代码也能正常本地化): xaml中使用 引入资源文件命名空间 具体使用如 多终端资源管理与检测系统,包含多个子进程模块,目前只开发了xamarin.forms客户端,下一步开发服务端,使用 .net 5 web api开发,基于abp vnext搭建。 作为terminalmacs系统的一个子进程模块,目前只开发了手机基本信息获取、联系人信息获取、本地化功能,后续开发服务端时,会配合添加通信功能,比如连接服务端验证、主动推送已获取资源等。[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
// the contact service processes the result of the permission request.
contactsservice.onrequestpermissionsresult(requestcode, permissions, grantresults);
2.2.3. ios
2.2.3.1. contactsservice.cs
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
2.3. 应用本地化
[assembly: neutralresourceslanguage("zh-cn")]
xmlns:resources="clr-namespace:terminalmacs.clients.app.resx"
<label text="{x:static resources:appresource.clientname_aboutpage}" fontattributes="bold"/>
3. 关于terminalmacs及本客户端
3.1. termainmacs
3.2. xamarin.forms客户端
3.3. 关于项目开源
下一篇: PHP实现的链式队列结构示例