Search code examples
c#mauifilesystemwatcher

How to use Clipboard.Default.SetTextAsync in MAUI with FileSystemWatcher


I have followed https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/data/clipboard?view=net-maui-7.0 in my project, and I understand that:

Access to the clipboard must be done on the main user interface thread. For more information on how to invoke methods on the main user interface thread, see MainThread.

I cannot do Clipboard.Default.SetTextAsync(url) without errors, and I can't understand where I've done wrong.

  • The URL is generated through the OnCreated() method of FileSystemWatcher.
  • I understand this is not the UI thread, but I fire an event which I believe should enable UI thread operations

I would like to get this code below fixed so that I can my app in macOS without any issues.

You can also check the code in GitHub by pulling the following two projects:

My code is as below.

using HelpersLib;
using Microsoft.Extensions.Logging;
using ShareX.HelpersLib;
using ShareX.UploadersLib;

namespace UploaderX;

public partial class MainPage : ContentPage
{
    int count = 0;
    private FileSystemWatcher _watcher;

    private string _watchDir;
    private string _destDir;

    public delegate void UrlReceivedEventHandler(string url);
    public event UrlReceivedEventHandler UrlReceived;

    public MainPage()
    {
        InitializeComponent();

        string AppDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "UploaderX");
        string AppSettingsDir = Path.Combine(AppDir, "Settings");
        App.Settings = ApplicationConfig.Load(Path.Combine(AppSettingsDir, "ApplicationConfig.json"));
        App.UploadersConfig = UploadersConfig.Load(Path.Combine(AppSettingsDir, "UploadersConfig.json"));
        App.UploadersConfig.SupportDPAPIEncryption = false;

        DebugHelper.Init(Path.Combine(AppDir, $"UploaderX-{DateTime.Now.ToString("yyyyMMdd")}-Log.txt"));

        _watchDir = Directory.Exists(App.Settings.CustomScreenshotsPath2) ? App.Settings.CustomScreenshotsPath2 : Path.Combine(AppDir, "Watch Folder");
        Helpers.CreateDirectoryFromDirectoryPath(_watchDir);

        _destDir = _watchDir;

        DebugHelper.Logger.WriteLine("Watch Dir: " + _watchDir);
        DebugHelper.Logger.WriteLine("Destination Dir: " + _destDir);

        _watcher = new FileSystemWatcher();
        _watcher.Path = _watchDir;

        _watcher.NotifyFilter = NotifyFilters.FileName;
        _watcher.Created += OnCreated;
        _watcher.EnableRaisingEvents = true;

        this.UrlReceived += MainPage_UrlReceived;

    }

    private async void MainPage_UrlReceived(string url)
    {
        await Clipboard.Default.SetTextAsync(url);
    }

    private void OnUrlReceived(string url)
    {
        UrlReceived?.Invoke(url);
    }

    private async void OnCounterClicked(object sender, EventArgs e)
    {
        count++;

        if (count == 1)
            CounterBtn.Text = $"Clicked {count} time";
        else
            CounterBtn.Text = $"Clicked {count} times";

        await Clipboard.Default.SetTextAsync(CounterBtn.Text);
    }

    async void OnCreated(object sender, FileSystemEventArgs e)
    {
        try
        {
            string fileName = new NameParser(NameParserType.FileName).Parse("%y%mo%d_%ra{10}") + Path.GetExtension(e.FullPath);
            string destPath = Path.Combine(Path.Combine(Path.Combine(_destDir, DateTime.Now.ToString("yyyy")), DateTime.Now.ToString("yyyy-MM")), fileName);
            FileHelpers.CreateDirectoryFromFilePath(destPath);
            if (!Path.GetFileName(e.FullPath).StartsWith("."))
            {
                int successCount = 0;
                long previousSize = -1;

                await Helpers.WaitWhileAsync(() =>
                {
                    if (!FileHelpers.IsFileLocked(e.FullPath))
                    {
                        long currentSize = FileHelpers.GetFileSize(e.FullPath);

                        if (currentSize > 0 && currentSize == previousSize)
                        {
                            successCount++;
                        }

                        previousSize = currentSize;
                        return successCount < 4;
                    }

                    previousSize = -1;
                    return true;
                }, 250, 5000, () =>
                {
                    File.Move(e.FullPath, destPath, overwrite: true);
                }, 1000);

                WorkerTask task = new WorkerTask(destPath);
                UploadResult result = task.UploadFile();
                DebugHelper.Logger.WriteLine(result.URL);
                OnUrlReceived(result.URL);
            }
        }
        catch (Exception ex)
        {
            DebugHelper.Logger.WriteLine(ex.Message);
        }

    }
}

Error once URL is generated:

enter image description here


Solution

  • You can wrap your call to SetTextAsync() with MainThread.BeginInvokeOnMainThread() like so to make sure it's invoked correctly:

    private void MainPage_UrlReceived(string url)
    {
        MainThread.BeginInvokeOnMainThread(() => 
        {
            Clipboard.Default.SetTextAsync(url);
        });
    }
    

    There is no need to await the call to SetTextAsync(), either, because you're calling it as a singular operation from within an event handler, which isn't awaitable.

    I understand this is not the UI thread, but I fire an event which I believe should enable UI thread operations

    Generally, there is no guarantee that event handlers of UI classes are called on the Main Thread.

    You can find more information on when and how to invoke methods on the Main Thread in the official documentation: https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/appmodel/main-thread?view=net-maui-7.0