Xamarin.Forms Client First Edition

As a sub-process module of TerminalMACS, the first edition is currently completed: reading basic mobile information, contact information, application localization.

  1. Functional introduction
  2. Detailed Functional Description
  3. About TerminalMACS

1. Functional introduction

1.1. Read basic mobile information

Use the Xamarin.Essentials library to get basic device information, and the Xam.Plugin.DeviceInfo plug-in to get App Id. In fact, the plug-in can also get basic device information.

1.2. Read mobile contact information

The Android and iOS projects specifically implement contact reading services, using DependencyService to get service capabilities.

1.3. Application localization

Only Chinese and English have been localized using resource files.

2. Detailed functional description

2.1. Read basic mobile information

The Xamarin.Essentials library is used to get basic information about the mobile phone, such as the manufacturer, model, name, type, version, etc. The Xam.Plugin.DeviceInfo plug-in gets App Id to uniquely identify different mobile phones. See the following image for information:

The code structure is as follows:

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. Read mobile contact information

The Android and iOS projects specifically implement the Contact Read service, using DependencyService to get service capabilities, with the following screenshots:

2.2.1. TerminalMACS.Clients.App

The code structure is as follows:

2.2.1.1. Contact Entity Class: Contacts.cs

Currently only contact names, pictures, e-mail (possibly multiple), phone numbers (possibly multiple), and more can be extended.

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. Contact Service Interface: IContactsService.cs

Include:

  • A Contact Gets Request Interface: RetrieveContactsAsync
  • A read contact result notification event: OnContactLoaded

The interface is implemented by a specific platform (Android and 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. Contact VM: ContactViewModel.cs

VM provides the following two functions:

  1. Load all contacts.
  2. Contact keyword query.
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 Contact display page: ContactPage.xaml

Simple layouts, a StackLayout layout container arranged vertically, and a SearchBar providing keyword search capabilities.

<?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

The code structure is as follows:

  • AndroidManifest.xml: Write read and write contact rights requests.
  • ContactsService.cs: Specific contact rights requests, data read operations.
  • MainActivity.cs: Receive permission request results
  • MainApplicaion.cs: This class does not add task-critical code, but it is essential or the permission request window will not pop up correctly.
  • PermissionUtil.cs: Permission Request Result Judgment

2.2.2.1. Add permissions to AndroidManifest.xml

Just add the following line:

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

2.2.2.2. ContactsService.cs

Android contacts get implementation services to implement IContactsService.Note the attribute code on the namespace, which must be added before you can use DependencyService.Get() to obtain this service instance in the previous contact VM. The default service is singleton:

[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 code is simple and only receives permission request results in the OnRequestPermissionsResult method:

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

2.2.3. iOS

The code structure is as follows:

  • ContactsService.cs: Specific contact rights requests, data read operations.
  • Info.plist: Describe file when permission is requested

2.2.3.1. ContactsService.cs

IOS specific contact reading service, which implements the IContactsService interface, works like Android Contact Service, I have no debugging environment, iOS this function is not tested.

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

Contact Rights Request Description

2.3. Application localization

Only Chinese and English have been localized using resource files.

The resource files are as follows:

Specify default culture

For the resource file to work properly, the application must specify NeutralResourcesLanguage.In shared projects, the AssemblyInfo.cs file should be customized to specify the default culture.The following code demonstrates how to set NeutralResourcesLanguage to zh-CN in the AssemblyInfo.cs file (extracted from official documents: https://docs.microsoft.com/zh-cn/samples/xamarin/xamarin-forms-samples/usingresxlocalization/, after testing, this code can also be localized properly):

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

Use in XAML

Introducing a resource file namespace

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

Specific uses such as

<Label Text="{x:Static resources:AppResource.ClientName_AboutPage}" FontAttributes="Bold"/>

3. About TerminalMACS and this client

3.1. TermainMACS

The multi-terminal resource management and detection system, which contains several sub-process modules, has only developed Xamarin.Forms client at present. The next step is to develop the server, which is based on Abp vNext using.NET 5 Web API.

3.2. Xamarin.Forms Client

As a sub-process module of TerminalMACS system, only the basic information acquisition, contact information acquisition and localization functions of mobile phone have been developed at present. When developing service side in the future, they will cooperate with adding communication functions, such as connecting service side validation, actively pushing acquired resources, etc.

3.3. About open source projects

Keywords: C# iOS Android Mobile xml

Added by vargadanis on Sat, 04 Apr 2020 14:42:57 +0300