Xamarin.Forms client first version

Original: First version of Xamarin.Forms client

Xamarin.Forms client first version

As a sub-process module of TerminalMACS, the first version is currently completed: reading and displaying basic phone information, contact information, and application localization.

  1. Function introduction
  2. Detailed function description
  3. About TerminalMACS

1. Function Introduction

1.1. Read the basic information of the mobile phone

The Xamarin.Essentials library is mainly used to obtain basic device information, and the Xam.Plugin.DeviceInfo plug-in is used to obtain App Id. In fact, the plug-in can also obtain basic device information.

1.2. Read phone contact information

The Android and iOS projects specifically implement the contact reading service, and use DependencyService to obtain the service function.

1.3. Application localization

Using resource files for localization, currently only in Chinese and English.

2. Detailed function description

2.1. Read basic information of mobile phone

The Xamarin.Essentials library is used to obtain the basic information of the mobile phone, such as the mobile phone manufacturer, model, name, type, version, etc .; the Xam.Plugin.DeviceInfo plugin is used to obtain the App Id, which is used to uniquely identify different mobile phones. For the information, see the following figure:

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 phone contact information

The Android and iOS projects specifically implement the contact reading service, and use the DependencyService to obtain the service function. The function screenshot is as follows:

2.2.1. TerminalMACS.Clients.App

The code structure is as follows:

2.2.1.1. Contact entity class: Contacts.cs

Currently only the contact name, picture, email (may be multiple), phone number (may be multiple) are obtained, and more can be expanded.

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 acquisition request interface: RetrieveContactsAsync
  • A notification event for reading a contact result: OnContactLoaded

The interface is implemented by specific platforms (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. All contacts are loaded.
  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 layout, a StackLayout layout container is arranged vertically, and a SearchBar provides keyword search function.

<?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 permission request for reading and writing contacts.
  • ContactsService.cs: specific contact permission request and data reading operation.
  • MainActivity.cs: Receive permission request results
  • MainApplicaion.cs: No mission-critical code is added to this class, but it is essential, otherwise the permission request window will not pop up properly.
  • PermissionUtil.cs: Permission request result judgment

2.2.2.1. Add permission to AndroidManifest.xml

Just add the following line:

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

2.2.2.2. ContactsService.cs

Android contact acquisition and realization service, realize IContactsService. Pay attention to the feature code in the namespace. After adding this feature, you can use DependencyService.Get in the previous contact VM () Get this service instance, 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, only receive the permission request result 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 permission request and data reading operation.
  • Info.plist: description file when requesting permission

2.2.3.1. ContactsService.cs

The iOS specific contact reading service implements the IContactsService interface. The principle is similar to the Android contact service. I have no debugging environment. This function has not been tested in 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

Contact permission request description
Info.plist

2.3. Application localization

Using resource files for localization, currently only in Chinese and English.

The resource file is as follows:

Specify the default culture

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

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

Used in XAML

Introduce resource file namespace

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

Specific use 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 contains multiple sub-process modules. Currently, only the Xamarin.Forms client is developed. The next step is to develop the server. It is developed using .NET 5 Web API and is built on Abp vNext.

3.2. Xamarin.Forms client

As a sub-process module of the TerminalMACS system, currently only the basic information acquisition, contact information acquisition, and localization functions of the mobile phone are developed. When the server is subsequently developed, communication functions will be added, such as connection server verification and active push of acquired resources Wait.

3.3. About project open source

Guess you like

Origin www.cnblogs.com/lonelyxmas/p/12672746.html