Home > front end >  How do I properly use Clipboard in a console application?
How do I properly use Clipboard in a console application?

Time:10-25

I am a C# beginner and I am trying to create a Windows Service (Console Application with Topshelf) (.Net Framwork 4.8) that gets and sets the clipboard every second (yes, useless service, it's just for learning).

When using System.Windows.Forms as reference in my service class the Timer class stops working ("'Timer' is an ambiguous reference between 'System.Windows.Forms.Timer' and 'System.Timers.Timer'") and the application throws System.Threading.ThreadStateException at the lines where I'm using the Clipboard class: "Current thread must be set to single thread apartment (STA) mode before OLE calls can be made, ensure that your Main function has STAThreadAttribute marked on it".

using System.Timers;
using System.Windows.Forms;

namespace ClipboardProject
{
    public class TimerClipboard
    {
        private readonly Timer _timer;

        public TimerClipboard()
        {
            _timer = new Timer(1000) { AutoReset = true };
            _timer.Elapsed  = TimerElapsed;
        }

        private void TimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            string userClipboard = Clipboard.GetText();
            Clipboard.SetText($"Latest copy: {userClipboard}");
        }

        public void Start()
        {
            _timer.Start();
        }

        public void Stop()
        {
            _timer.Stop();
        }
    }
}

What am I doing wrong?

Edited:

This is my main method.

using System;
using Topshelf;

namespace ClipboardProject
{
    public class Program
    {
        static void Main(string[] args)
        {
            var exitCode = HostFactory.Run(x =>
             {
                 x.Service<TimerClipboard>(s =>
                 {
                     s.ConstructUsing(timerClipboard=> new TimerClipboard());
                     s.WhenStarted(timerClipboard=> timerClipboard.Start());
                     s.WhenStopped(timerClipboard=> timerClipboard.Stop());
                 });

                 x.RunAsLocalSystem();
                 x.SetServiceName("Random ServiceName");
                 x.SetDisplayName("Random DisplayName");
                 x.SetDescription("Random Description");
             });

             int exitCodeValue = (int)Convert.ChangeType(exitCode, exitCode.GetTypeCode());
             Environment.ExitCode = exitCodeValue;
        }
    }
}

CodePudding user response:

System.Timers.Timer.Elapsed handler always running in background thread, so Clipboard OLE calls causes System.Threading.ThreadStateException.

You can handle it by creating new System.Threading.Thread and force it to start as STA thread by SetApartmentStart(ApartmentState.STA), but this is unefficient, horrible and wrong solution:

private void TimerElapsed(object sender, ElapsedEventArgs e)
{
    System.Threading.Thread t = new System.Threading.Thread(() =>
    {
        string userClipboard = Clipboard.GetText();
        Clipboard.SetText($"Latest copy: {userClipboard}");
    });

    t.SetApartmentState(System.Threading.ApartmentState.STA);
    t.Start();
}

because on each interval tick it would create new Thread. Threads take huge performance-cost, especially if created in loops.

So, the correct solution may be in usage of System.Windows.Threading.DispatcherTimer from PresentationFramework library:

public class TimerClipboard
{
    private readonly System.Windows.Threading.DispatcherTimer _timer;

    public TimerClipboard()
    {
        _timer = new System.Windows.Threading.DispatcherTimer();
        _timer.Interval = TimeSpan.FromSeconds(1);
        _timer.Tick  = OnDispatcherTimerTick;
    }

    private void OnDispatcherTimerTick(object sender, EventArgs e)
    {
        string userClipboard = Clipboard.GetText();
        Clipboard.SetText($"Latest copy: {userClipboard}");
    }

    public void Start() => _timer.Start();
    public void Stop() => _timer.Stop();
}

About "'Timer' is an ambiguous reference between System.Windows.Forms.Timer and System.Timers.Timer": that's because both namespaces has class Timer, so your Studio doesn't know which one you want and need to use. It can be solved by removing another using or with explicit specifying:

using Timer = System.Timers.Timer;
// or
using Timer = System.Windows.Forms.Timer;

namespace ClipboardProject
{
    // ...
}

EDIT.

As @Hans Passant noticed, DispatcherTimer.Tick event couldn't fire without dispatcher, so upper solution won't work with Topshelf. So I offer to rewrite TimerClipboard class to remove any timer from there and use simple flag-based while loop. As there no timer, I renamed it to ClipboardWorker.

Complete solution looks like this:

using System;
using System.Threading.Tasks;
using System.Windows.Forms;
using Topshelf;

namespace ClipboardProject
{
    class Program
    {
        // Add STA attribute to Main method
        [STAThread] 
        static void Main(string[] args)
        {
            var topshelfExitCode = HostFactory.Run(x =>
            {
                x.Service<ClipboardWorker>(s =>
                {
                    s.ConstructUsing(cw => new ClipboardWorker());
                    s.WhenStarted(cw => cw.Start());
                    s.WhenStopped(cw => cw.Stop());
                });

                x.RunAsLocalSystem();
                x.SetServiceName("ClipboardWorkerServiceName");
                x.SetDisplayName("ClipboardWorkerDisplayName");
                x.SetDescription("ClipboardWorkerDescription");
            });

            Environment.ExitCode = (int)topshelfExitCode;
        }
    }

    public class ClipboardWorker
    {
        // Flag that would indicate our Worker in running or not
        private bool _isRunning;
        private int _interval = 1000; // Default value would be 1000 ms

        public bool IsRunning { get => _isRunning; }
        public int Interval 
        { 
            get => _interval;
            // Check value which sets is greater than 0. Elseway set default 1000 ms
            set => _interval = value > 0 ? value : 1000;
        }

        // Constructor
        public ClipboardWorker() 
        {
            Console.WriteLine();
            Console.WriteLine("ClipboardWorker initialized.");
        }

        // "Tick" simulation. 
        private void DoWorkWithClipboard()
        {
            // Loop runs until Stop method would be called, which would set _isRunning to false
            while (_isRunning)
            {
                Console.WriteLine(); // <--- just line break for readability

                string userClipboard = Clipboard.GetText();
                Console.WriteLine($"Captured from Clipboard value: {userClipboard}");

                Clipboard.SetText($"Latest copy: {userClipboard}");
                Console.WriteLine($"Latest copy: {userClipboard}");

                // Use delay as interval between "ticks"
                Task.Delay(Interval).Wait();               
            }
        }

        public void Start()
        {
            // Set to true so while loop in DoWorkWithClipboard method be able to run
            _isRunning = true;
            Console.WriteLine("ClipboardWorker started.");
            // Run "ticking"
            DoWorkWithClipboard();          
        }

        public void Stop()
        {
            // Set to false to break while loop in DoWorkWithClipboard method
            _isRunning = false;
            Console.WriteLine("ClipboardWorker stopped.");
        }
    }
}

Sample output:

enter image description here

  • Related