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
. Thread
s 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: