Creating a Remote Desktop Client Application without using Windows Forms (C#)

crankedrelic picture crankedrelic · Dec 11, 2017 · Viewed 8.1k times · Source

I need to build a Remote Desktop Client application with C#, which establishes a connection to a remote Windows Server, and then programmatically starts some services to the remote PC.

It's important that, when I logon, the Desktop Environment on the Server side exists, because the services I want to start make use of it, but on the client side I don't want any Windows Forms container, because I want to create these sessions dynamically.

To understand the question better, imagine that i want to establish a Remote Desktop Connection, using a console application. The point is, in the client side I don't need any GUI, but the services on the Host side need the windows, mouse, internet explorer etc UI handles.

So far I tried to use the MSTSClib to create an RdpClient as discribed here, but that didn't help, because it makes use of the AxHost, which is Windows Forms dependent.

Any ideas on if that's possible, and how can I achieve that?

UPDATE:

Tried this:

using System;
using AxMSTSCLib;
using System.Threading;
using System.Windows.Forms;

namespace RDConsole
{
    class Program
    {
        static void Main(string[] args)
        {

            var thread = new Thread(() =>
                {
                    var rdp = new AxMsRdpClient9NotSafeForScripting();
                    rdp.CreateControl();
                    rdp.OnConnecting += (s, e) => { Console.WriteLine("connecting"); };
                    rdp.Server = "xxx.xxx.xxx.xxx";
                    rdp.UserName = "Administrator";
                    rdp.AdvancedSettings9.AuthenticationLevel = 2;
                    rdp.AdvancedSettings9.ClearTextPassword = "xxxxxxxxxx";
                    rdp.Connect();
                    Console.ReadKey();
                });
            thread.SetApartmentState(ApartmentState.STA);
            thread.IsBackground = true;
            thread.Start();
            Console.ReadKey();
        }


    }
}

but i get a null reference exception

"Object reference not set to an instance of an object.

Answer

crankedrelic picture crankedrelic · May 21, 2019

Finally, I'm posting the answer to this. This is the wrapper for the remote control library, together with the WinForms-like message loop. You still have to reference the windows forms dll and create a form to host the rdpclient, but this now can run from a console app, a windows service, or whatever.

using AxMSTSCLib;

public class RemoteDesktopApi
{

    #region Methods

    public void Connect((string username, string domain, string password, string machineName) credentials)
    {
        try
        {
            var form = new Form();
            var remoteDesktopClient = new AxMsRdpClient6NotSafeForScripting();
            form.Controls.Add(remoteDesktopClient);
            form.Show();

            remoteDesktopClient.AdvancedSettings7.AuthenticationLevel = 0;
            remoteDesktopClient.AdvancedSettings7.EnableCredSspSupport = true;
            remoteDesktopClient.Server = credentials.machineName;
            remoteDesktopClient.Domain = credentials.domain;
            remoteDesktopClient.UserName = credentials.username;
            remoteDesktopClient.AdvancedSettings7.ClearTextPassword = credentials.password;
            remoteDesktopClient.Connect();
        }
        catch (Exception e)
        {
            throw new Exception(e.Message);
        }
    }

    #endregion

    #region Nested type: MessageLoopApartment

    public class MessageLoopApartment : IDisposable
    {
        #region  Fields/Consts

        private static readonly Lazy<MessageLoopApartment> Instance = new Lazy<MessageLoopApartment>(() => new MessageLoopApartment());
        private TaskScheduler _taskScheduler;
        private Thread _thread;

        #endregion

        #region  Properties

        public static MessageLoopApartment I => Instance.Value;

        #endregion

        private MessageLoopApartment()
        {
            var tcs = new TaskCompletionSource<TaskScheduler>();

            _thread = new Thread(startArg =>
            {
                void IdleHandler(object s, EventArgs e)
                {
                    Application.Idle -= IdleHandler;
                    tcs.SetResult(TaskScheduler.FromCurrentSynchronizationContext());
                }

                Application.Idle += IdleHandler;
                Application.Run();
            });

            _thread.SetApartmentState(ApartmentState.STA);
            _thread.IsBackground = true;
            _thread.Start();
            _taskScheduler = tcs.Task.Result;
        }

        #region IDisposable Implementation

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        #endregion

        #region Methods

        public Task Run(Action action, CancellationToken token)
        {
            return Task.Factory.StartNew(() =>
            {
                try
                {
                    action();
                }
                catch (Exception)
                {
                    // ignored
                }
            }, token, TaskCreationOptions.LongRunning, _taskScheduler);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (_taskScheduler == null) return;

            var taskScheduler = _taskScheduler;
            _taskScheduler = null;
            Task.Factory.StartNew(
                    Application.ExitThread,
                    CancellationToken.None,
                    TaskCreationOptions.None,
                    taskScheduler)
                .Wait();
            _thread.Join();
            _thread = null;
        }

        #endregion
    }

    #endregion
}

and this is how I call the Connect method

public void ConnectToRemoteDesktop((string username, string domain, string password, string machineName) credentials)
    {
        RemoteDesktopApi.MessageLoopApartment.I.Run(() =>
        {
            var ca = new RemoteDesktopApi();
            ca.Connect(credentials);
        }, CancellationToken.None);
    }

This may also be useful with other types ActiveX controls.