Xamarin.Forms客戶端第一版 作為TerminalMACS的一個子進程模塊,目前完成第一版:讀取展示手機基本信息、聯繫人信息、應用程式本地化。 1. 功能簡介 2. 詳細功能說明 3. 關於TerminalMACS 1. 功能簡介 1.1. 讀取手機基本信息 主要使用Xamarin.Ess ...
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
[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
聯繫人許可權請求說明
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. 關於項目開源
-
- 合作網站:https://dotnet9.com