csharp/admaiorastudio/realxaml/RealXaml.Client/AppManager.cs

AppManager.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;
using System.Net.NetworkInformation;
using Microsoft.AspNetCore.SignalR.Client;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
using Xamarin.Forms.Internals;
using System.IO;
using System.IO.Compression;

namespace AdMaiora.RealXaml.Client
{
    public sealed clast AppManager
    {
        #region Constants and Fields

        private static Lazy _current = new Lazy();

        private Action _init;

        private WeakReference _app;
        private Dictionary _pages;        
        
        private string _serverAddress;

        private bool _isConnected;
        private bool _useLocalHost;

        private TaskCompletionSource _connectionTCS;

        private HubConnection _hubConnection;

        private Dictionary _xamlCache;

        #endregion

        #region Properties

        internal static AppManager Current
        {
            get
            {
                return _current.Value;
            }
        }

        public bool IsConnected
        {
            get
            {
                return _isConnected;
            }
        }

        #endregion

        #region Constructors

        public AppManager()
        {
            _pages = new Dictionary();
            _xamlCache = new Dictionary();
        }

        #endregion

        #region Public Methods

        public static void Init(Application application)
        {
            AppManager.Current.Setup(application);
        }

        public static void Init(Page page)
        {
            AppManager.Current.Setup(page);
        }

        public static void Debug(string message)
        {
            Task.Run(async () => 
                await AppManager.Current.OutputDebugMessage(message));
        }

        internal void Setup(Application application)
        {
            if (application == null)
                throw new ArgumentNullException("application");

            AppDomain.CurrentDomain.UnhandledException += this.CurrentDomain_UnhandledException;
            TaskScheduler.UnobservedTaskException += this.TaskScheduler_UnobservedTaskException;

            _app = new WeakReference(application);

            application.PageAppearing += Application_PageAppearing;
            application.PageDisappearing += Application_PageDisappearing;

            _connectionTCS = new TaskCompletionSource();
            Task.Run(
                async () =>
                {
                    try
                    {                        
                        await ConnectAsync();
                        if (_isConnected)
                        {
                            // Connection successfully estabilished
                            _connectionTCS.SetResult(_isConnected);
                        }
                        else
                        {
                            // Unable to connect, we should retry later
                            _connectionTCS.SetResult(_isConnected);
                            System.Diagnostics.Debug.WriteLine("Unable to connect to the RealXaml server.");

                            while (true)
                            {
                                System.Diagnostics.Debug.WriteLine("Trying to reconnect again...");
                                await ConnectAsync();
                                if (_isConnected)
                                {                                    
                                    break;
                                }

                                System.Diagnostics.Debug.WriteLine("Unable to connect. Retrying in 5secs.");
                                await Task.Delay(5000);                                
                            }
                        }
                    }
                    catch(Exception ex)
                    {                        
                        _connectionTCS.SetException(ex);
                    }
                });
        }

        internal void Setup(Page page)
        {
            string pageId = page.GetType().FullName;

            byte[] data = null;
            if(_xamlCache.TryGetValue(pageId, out data))
                Task.Run(async () => await ReloadXaml(page, data));
        }

        internal async Task OutputDebugMessage(string message)
        {
            if (_isConnected)
                await _hubConnection.SendAsync("ThrowException", message);
        }

        internal async Task MonitorExceptionAsync(Exception exception)
        {
            if (_isConnected)
                await _hubConnection.SendAsync("ThrowException", exception.ToString());
        }

        #endregion

        #region Methods

        private async Task ConnectAsync()
        {
            if (_isConnected)
                return;

            // Emulators loopback addresses
            IPAddress[] loopbackAddresses = new[]
            {
                IPAddress.Parse("127.0.0.1"),
                IPAddress.Parse("10.0.2.2"),
                IPAddress.Parse("10.0.3.2"),
                IPAddress.Parse("169.254.80.80")
            };

            // Check if we are an emulator instance
            List waitTasks = new List();
            CancellationTokenSource cts = new CancellationTokenSource();

            // Look for server using localhost (an emulator device)
            foreach (var ipAddress in loopbackAddresses.Take(1))
            {
                waitTasks.Add(Task.Run(
                    async () =>
                    {
                        try
                        {
                            bool isPortOpen = TryPing(ipAddress.ToString(), 5001, 300);
                            if (!isPortOpen)
                                return null;

                            var connection = new HubConnectionBuilder()
                                .WithUrl($"http://{ipAddress.ToString()}:5001/hub")
                                .Build();

                            await connection.StartAsync(cts.Token);
                            if (cts.IsCancellationRequested)
                                return null;

                            _useLocalHost = true;
                            _hubConnection = connection;

                            cts.Cancel();
                            return ipAddress.ToString();
                        }
                        catch (Exception ex)
                        {
                            return null;
                        }

                    }, cts.Token));
            }

            // Look for server using broadcast (a real device)
            waitTasks.Add(Task.Run(
                async () =>
                {
                    // Discover the server
                    using (UdpClient client = new UdpClient())
                    {
                        client.EnableBroadcast = true;

                        byte[] requestData = Encoding.ASCII.GetBytes($"AreYouTheServer?");
                        Task sendTask = client.SendAsync(requestData, requestData.Length, new IPEndPoint(IPAddress.Broadcast, 5002));
                        await Task.WhenAny(new[] { sendTask, Task.Delay(300) });
                        if (sendTask.IsCompleted)
                        {
                            if (cts.IsCancellationRequested)
                                return null;

                            Task receiveTask = client.ReceiveAsync();
                            await Task.WhenAny(new[] { receiveTask, Task.Delay(300) });
                            if (receiveTask.IsCompleted)
                            {
                                if (cts.IsCancellationRequested)
                                    return null;

                                UdpReceiveResult serverResponseData = receiveTask.Result;
                                string serverResponse = Encoding.ASCII.GetString(serverResponseData.Buffer);
                                if (serverResponse == "YesIamTheServer!")
                                {
                                    string ipAddress = serverResponseData.RemoteEndPoint.Address.ToString();
                                    _useLocalHost = false;
                                    _hubConnection = null;

                                    cts.Cancel();
                                    return ipAddress.ToString();

                                }
                            }
                        }

                        client.Close();
                    }

                    return null;
                }));

            // Timeout task 
            waitTasks.Add(Task.Run(
                async () =>
                {
                    try
                    {
                        await Task.Delay(5000, cts.Token);
                        cts.Cancel();
                        return null;
                    }
                    catch
                    {
                        return null;
                    }
                }));

            try
            {
                string ipAddress = await WaitForAnyGetHostIpTaskAsync(waitTasks);
                if (ipAddress != null)
                {
                    if (_hubConnection == null)
                    {
                        string port = _useLocalHost ? "5001" : "5002";
                        _hubConnection = new HubConnectionBuilder()
                            .WithUrl($"http://{ipAddress.ToString()}:{port}/hub")
                            .Build();

                        await _hubConnection.StartAsync();
                    }

                    _isConnected = true;
                    _serverAddress = ipAddress;

                    _hubConnection.Closed +=
                        async (error) =>
                        {
                            System.Diagnostics.Debug.WriteLine("Connection with RealXaml has been lost.");                            

                            while(_hubConnection.State == HubConnectionState.Disconnected)
                            {
                                bool isPortOpen = TryPing(ipAddress.ToString(), 5001, 300);
                                if (isPortOpen)
                                {
                                    System.Diagnostics.Debug.WriteLine("Trying to reconnect again...");
                                    await _hubConnection.StartAsync();
                                    if (_hubConnection.State == HubConnectionState.Connected)
                                    {
                                        await Task.Delay(300);
                                        await _hubConnection.SendAsync("NotifyIde", "Connection was lost. Here I'am again.");

                                        System.Diagnostics.Debug.WriteLine($"Successfully restored lost to the RealXaml server.");
                                        break;
                                    }
                                }

                                System.Diagnostics.Debug.WriteLine("Unable to connect. Retrying in 5secs.");
                                await Task.Delay(5000);
                            }
                        };                    

                    _hubConnection.On("ReloadXaml", 
                        async (pageId, data, refresh) => await WhenReloadXaml(pageId, data, refresh));

                    _hubConnection.On("Reloadastembly", 
                        async (astemblyName, data) => await WhenReloadastembly(astemblyName, data));

                    string clientId = $"RXID-{DateTime.Now.Ticks}";
                    await _hubConnection.SendAsync("RegisterClient", clientId);

                    System.Diagnostics.Debug.WriteLine($"Successfully connected to the RealXaml server.");
                    System.Diagnostics.Debug.WriteLine($"Your client ID is {clientId}");

                    return;
                }
            }
            catch(Exception ex)
            {
                System.Diagnostics.Debug.WriteLine("Error while trying to connect to the RealXaml server.");
                System.Diagnostics.Debug.WriteLine(ex);
            }       
        }

        private async Task WaitForAnyGetHostIpTaskAsync(IEnumerable tasks)
        {
            IList customTasks = tasks.ToList();
            Task completedTask;
            string ipAddress = null;

            do
            {
                completedTask = await Task.WhenAny(customTasks);
                ipAddress = completedTask.Result;
                customTasks.Remove(completedTask);

            } while (ipAddress == null && customTasks.Count > 0);

            return ipAddress;
        }

        private async Task ReloadXaml(Page page, byte[] data = null)
        {
            try
            {
                Type pageType = page.GetType();
                MethodInfo configureAfterLoadMethodInfo =
                    pageType.GetMethods(
                      BindingFlags.Public
                    | BindingFlags.NonPublic
                    | BindingFlags.Static
                    | BindingFlags.Instance
                    | BindingFlags.InvokeMethod)
                    .Where(x => x.CustomAttributes.Any(y => y.AttributeType == typeof(RunAfterXamlLoadAttribute)))
                    .SingleOrDefault();

                bool useAsyncConfigureAfterLoad = (configureAfterLoadMethodInfo?.CustomAttributes
                    .Any(x => x.AttributeType == typeof(AsyncStateMachineAttribute))).GetValueOrDefault(false);

                Device.BeginInvokeOnMainThread(
                    async () =>
                    {
                        try
                        {                            
                            // We need to clear tool bar items
                            // because reloading the xaml seems
                            // not resetting the buttons
                            page.ToolbarItems?.Clear();

                            // Reload the xaml!
                            page.Resources.Clear();
                            if (data != null)
                            {
                                string xaml = DecompressXaml(data);
                                page.LoadFromXaml(xaml);
                            }
                            else
                            {
                                string pageId = page.GetType().FullName;
                                if (_xamlCache.TryGetValue(pageId, out data))
                                {
                                    string xaml = DecompressXaml(data);
                                    page.LoadFromXaml(xaml);
                                }
                            }

                            // Configure after xaml reload
                            // This is usally needed when some data is binded via code                        
                            if (useAsyncConfigureAfterLoad)
                            {
                                await (Task)configureAfterLoadMethodInfo?.Invoke(page, null);
                            }
                            else
                            {
                                configureAfterLoadMethodInfo?.Invoke(page, null);
                            }

                            // Notify that the xaml was loaded correctly
                            await _hubConnection.SendAsync("XamlReloaded", page.GetType().FullName, data);

                            System.Diagnostics.Debug.WriteLine($"Page '{page.GetType().FullName}' received a new xaml.");
                        }
                        catch (Exception ex)
                        {
                            // Notify that something went wrong
                            await _hubConnection.SendAsync("ThrowException", ex.ToString());

                            System.Diagnostics.Debug.WriteLine($"Unable to update the xaml for page '{page.GetType().FullName}'");
                            System.Diagnostics.Debug.WriteLine(ex);
                        }
                    });
            }
            catch (Exception ex)
            {
                // Notify that something went wrong
                await _hubConnection.SendAsync("ThrowException", ex.ToString());

                System.Diagnostics.Debug.WriteLine($"Unable to update the xaml for page '{page.GetType().FullName}'");
                System.Diagnostics.Debug.WriteLine(ex);
            }
        }

        private async Task Reloadastmbly(string astemblyName, byte[] data)
        {
            try
            {
                data = DecompressData(data);
                astembly astembly = astembly.Load(data);

                /*
                 * We use the two attributes MainPage and RootPage to let RealXaml know
                 * how he need to restart our application on astembly reload. 
                 * Different scenario are possibile:
                 * 
                 * At least we need to define one MainPage (for single page, no navigation)
                 * or we need to define one RootPage (for multi page with navigation). 
                 * When defining only a RootPage, a NavigationPage will be used as MainPage.
                 * 
                 * We can use them both to specify which clast will be used as MainPage and RootPage.
                 * Using them togheter means that your custom MainPage needs to have a RootPage specified in the constructor.
                 * 
                 */

                Type mainPageType = astembly.GetTypes()
                    .Where(x => x.CustomAttributes.Any(y => y.AttributeType == typeof(MainPageAttribute))).FirstOrDefault();

                Type rootPageType = astembly.GetTypes()
                    .Where(x => x.CustomAttributes.Any(y => y.AttributeType == typeof(RootPageAttribute))).FirstOrDefault();

                if (mainPageType == null && rootPageType == null)
                    throw new InvalidOperationException("Unable to create a new MainPage. Did you mark a page with the [MainPage] or the [RootPage] attribute? ");

                Application app = null;
                if (_app.TryGetTarget(out app))
                {
                    Device.BeginInvokeOnMainThread(
                        async () =>
                        {
                            try
                            {
                                Page rootPage = null;

                                // In case of single page, no navigation
                                if(mainPageType != null 
                                    && rootPageType == null)
                                {
                                    // Create the new main page
                                    app.MainPage = (Page)Activator.CreateInstance(mainPageType);
                                }
                                // In case of multi page with navigation
                                else if(rootPageType != null
                                    && mainPageType == null)
                                {
                                    mainPageType = typeof(NavigationPage);
                                    app.MainPage = new NavigationPage((Page)Activator.CreateInstance(rootPageType));
                                }
                                // In case of custom configuration
                                else if(mainPageType != null
                                    && rootPageType != null)
                                {
                                    // Create the new main page which must host a root page
                                    rootPage = (Page)Activator.CreateInstance(rootPageType);
                                    app.MainPage = (Page)Activator.CreateInstance(mainPageType, rootPage);

                                }

                                // Reset collected pages 
                                _pages.Clear();

                                // Re collect the root page
                                if (rootPageType != null)
                                {
                                    _pages.Add(rootPageType.FullName, new WeakReference(rootPage));
                                    await ReloadXaml(rootPage);
                                }

                                // Re collect the main page (could be a NavigationPage)
                                if (app.MainPage != null)
                                {
                                    _pages.Add(mainPageType.FullName, new WeakReference(app.MainPage));
                                    if (app.MainPage.GetType() != typeof(NavigationPage))
                                        await ReloadXaml(app.MainPage);
                                }

                                // Notify that the astembly was loaded correctly
                                await _hubConnection.SendAsync("astemblyReloaded", astemblyName, astembly.GetName().Version.ToString());

                                System.Diagnostics.Debug.WriteLine($"A new main page of type '{mainPageType.FullName}' has been loaded!", "Ok");
                            }
                            catch (Exception ex)
                            {
                                // Notify that the astembly was loaded correctly
                                await _hubConnection.SendAsync("ThrowException", ex.ToString());

                                System.Diagnostics.Debug.WriteLine($"Unable to load the astembly '{astemblyName}'");
                                System.Diagnostics.Debug.WriteLine(ex);
                            }
                        });
                }
            }
            catch (Exception ex)
            {
                // Notify that the astembly was loaded correctly
                await _hubConnection.SendAsync("ThrowException", ex.ToString());

                System.Diagnostics.Debug.WriteLine($"Unable to load the astembly '{astemblyName}'");
                System.Diagnostics.Debug.WriteLine(ex);
            }
        }

        private byte[] CompressData(byte[] data)
        {
            using (MemoryStream memory = new MemoryStream())
            {
                using (GZipStream gzip = new GZipStream(memory, CompressionLevel.Fastest))
                {
                    gzip.Write(data, 0, data.Length);
                }

                return memory.ToArray();
            }
        }

        private byte[] CompressXaml(string xaml)
        {
            return CompressData(Encoding.ASCII.GetBytes(xaml));
        }

        private Byte[] DecompressData(byte[] data)
        {
            // Create a GZIP stream with decompression mode.
            // ... Then create a buffer and write into while reading from the GZIP stream.
            using (GZipStream stream = new GZipStream(new MemoryStream(data),
                CompressionMode.Decompress))
            {
                const int size = 4096;
                byte[] buffer = new byte[size];
                using (MemoryStream memory = new MemoryStream())
                {
                    int count = 0;
                    do
                    {
                        count = stream.Read(buffer, 0, size);
                        if (count > 0)
                        {
                            memory.Write(buffer, 0, count);
                        }
                    }
                    while (count > 0);
                    return memory.ToArray();
                }
            }
        }

        private string DecompressXaml(byte[] data)
        {
            return Encoding.ASCII.GetString(DecompressData(data));
        }

        private bool TryPing(string strIpAddress, int intPort, int nTimeoutMsec)
        {
            Socket socket = null;
            try
            {
                socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.DontLinger, false);


                IAsyncResult result = socket.BeginConnect(strIpAddress, intPort, null, null);
                bool success = result.AsyncWaitHandle.WaitOne(nTimeoutMsec, true);

                return socket.Connected;
            }
            catch
            {
                return false;
            }
            finally
            {
                if (null != socket)
                    socket.Close();
            }
        }

        #endregion

        #region SignalR Hub Callback Methods

        private async Task WhenReloadXaml(string pageId, byte[] data, bool refresh)
        {
            Application app = null;
            _app.TryGetTarget(out app);
            if (app == null)
                return;

            string xaml = DecompressXaml(data);

            astembly astembly = app.GetType().astembly;
            Type itemType = astembly.GetType(pageId);
            if (itemType != null
                && itemType.BaseType == typeof(Xamarin.Forms.Application))
            {
                Device.BeginInvokeOnMainThread(
                    () =>
                    {
                        app.Resources.Clear();
                        app.LoadFromXaml(xaml);
                    });
                // Do we need to refresh pages?
                if (refresh)
                {
                    var pages = _pages.Values
                        .Where(x => x.IsAlive)
                        .Select(x => x.Target as Page)
                        .Where(x => x.Parent != null)
                        .ToArray();

                    foreach (var page in pages)
                        await ReloadXaml(page);
                }
            }
            else
            {
                // For each page store the latest xaml
                // sended by the IDE or saved by the user. 
                // This allow every new page instace to have the latest xaml
                _xamlCache[pageId] = data;

                // Do we need to refresh pages?
                if (refresh)
                {
                    var pages = _pages.Values
                        .Where(x => x.IsAlive)
                        .Select(x => x.Target as Page)
                        .Where(x => x.Parent != null)
                        .ToArray();

                    foreach (var page in pages)
                    {
                        string pid = page.GetType().FullName;
                        if (pid == pageId)
                            await ReloadXaml(page, data);
                    }
                }
            }
        }

        private async Task WhenReloadastembly(string astemblyName, byte[] data)
        {
            await Reloadastmbly(astemblyName, data);
        }

        #endregion

        #region Event Handlers

        private async void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
        {
            if (_isConnected)
                await _hubConnection.SendAsync("ThrowException", e.Exception.ToString());
        }

        private async void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            if(_isConnected)
                await _hubConnection.SendAsync("ThrowException", (e.ExceptionObject as Exception)?.ToString());
        }

        private async void Application_PageAppearing(object sender, Page page)
        {
            if(!_isConnected)
                await _connectionTCS.Task;

            if(_isConnected)
            {
                string pageId = page.GetType().FullName;
                string pageKey = page.GetHashCode().ToString("x");
                if(!_pages.ContainsKey(pageKey))
                    _pages.Add(pageKey, new WeakReference(page));

                await _hubConnection.SendAsync("PageAppearing", pageId);
            }
        }

        private async void Application_PageDisappearing(object sender, Page page)
        {
            if (_isConnected)
            {
                string pageId = page.GetType().FullName;
                await _hubConnection.SendAsync("PageDisappearing", pageId);
            }
        }

        #endregion
    }
}