0

I have developed a little tool for my company. It's an WPF-Application. The aim is to open a pdf file from a specific folder, do some work and as soon as you saved the file, the application should enable some buttons to continue the work.

So my idea was to create a new thread, checking all view seconds if the LastWriteTime for the FileInfo was changed. As soon as that happens, the thread should enable all buttons which are not enabled yet.

privat FileInfo file; //File selected


private void Open_PDF(FileInfo file)
{
    if (file == null) return;
    Process.Start(file.FullName); //Open with standard pdf editor
    new Thread(New ThreadStart(Wait_FileSaved)).Start();
}


private void Wait_FileSaved()
{
    while (file.LastWriteTimeUtc.Equals(new FileInfo(file.FullName).LastWriteTimeUtc)
    {
        Thread.Sleep(2000);
    }
    
    Console.WriteLine("File saved"); //Just to check if it works
    EnableButtons(btnChecked, btnDenied, btnClarify, btnPass);
}


private void EnableButtons(params Button[] buttons)
{
    foreach (Button button in buttons)
        button.IsEnabled = true;    //Code fails here, buttons are created in another thread
                            //so I don't have access to the Buttons in the parallel thread
}

I have already found some results at google for this problem. According to the solutions there, I have changed my Code for EnableButtons:

private bool EnableButtons(params Button[] buttons)
{
    if (!this.Dispatcher.CheckAccess())
        return (bool)this.Dispatcher.Invoke((Func<Button[],bool>)EnableButtons, buttons);
    foreach (Button button in buttons)
        button.IsEnabled = true;
    return true;
    
}

I am pretty sure I use Dispatcher class wrong. Do I have to create one before threading? Is the code possible without threading? (and headache ^^)

You don't have to post Documentation for Dispatcher class, I've already read this, and still have no clue what this class is for and even if it is the right class to use in this case :)

Thanks guys, Micha

michi.wtr
  • 17
  • 4
  • 1
    i suggest you to use FileSystemwatcher -> https://learn.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher?redirectedfrom=MSDN&view=net-6.0 – Frenchy Apr 18 '23 at 07:22
  • @Frenchy also valuable your suggestion. – Mario Vernari Apr 18 '23 at 07:23
  • It's a bad idea to do `while (...) { Thread.Sleep(...); }`. – Enigmativity Apr 18 '23 at 08:07
  • @Enigmativity why is so? when i don't sleep my computer is busy. So i decided to sleep the thread. what would have been the nicer way then? thankful for every tip i could get :) – michi.wtr Apr 18 '23 at 08:42
  • @michi.wtr - A timer is always the better way. You can gracefully shut it down, but a sleeping thread is a wasted resource and it can't shut down. – Enigmativity Apr 18 '23 at 11:59

1 Answers1

0

Avoid using new threads when dealing with UI. Instead, just leverage async/await as follows:

private void Open_PDF(FileInfo file)
{
    if (file == null) return;
    Process.Start(file.FullName); //Open with standard pdf editor
    //new Thread(New ThreadStart(Wait_FileSaved)).Start();
    Wait_FileSaved();
}


private async Task Wait_FileSaved()
{
    while (file.LastWriteTimeUtc.Equals(new FileInfo(file.FullName).LastWriteTimeUtc)
    {
        await Task.Delay(2000);
    }
    
    Console.WriteLine("File saved"); //Just to check if it works
    EnableButtons(btnChecked, btnDenied, btnClarify, btnPass);
}


private void EnableButtons(params Button[] buttons)
{
    foreach (Button button in buttons)
        button.IsEnabled = true;    //Code fails here, buttons are created in another thread
                            //so I don't have access to the Buttons in the parallel thread
}

By using the async/await (Task), you won't block the UI thread.

Note that this is a super-simplified solution: definitely not the one I'd use in an app. However, I wouldn't use a while/delay to observe the result. Instead, I would use the Process termination event.

EDIT: due the below discussion, here is a variant where also the caller is async and should be awaited.

Here, I assume the following method as an event handler. Otherwise, the problem simply shifts upward in the call-stack. The async void should used only for event handlers. Just return Task and await it until a handler is met.

private async void Open_PDF(FileInfo file)
{
    if (file == null) return;
    Process.Start(file.FullName); //Open with standard pdf editor
    //new Thread(New ThreadStart(Wait_FileSaved)).Start();
    try
    {
        await Wait_FileSaved();
    }
    catch (Exception ex)   //make a better catcher
    {
        // ...handle
    }
}

This allows to start the inner Task, but also to catch exceptions.

Mario Vernari
  • 6,649
  • 1
  • 32
  • 44
  • Omg it was this easy :D ? I am completely new to programming with C# and also WPF, but didn't had a clue it works this fine, thx. PS: can't give U upvote due to my low score :\ – michi.wtr Apr 18 '23 at 07:34
  • well, thanks. However, I told you: that's a _simplified_ solution. In other words, the `Wait_FileSaved` function returns a `Task`. You should always "await" a Task, never call such a functions like I did above: that's not an error, but a discouraged approach. Have a careful read here, this guy writes very good bits: https://weblog.west-wind.com/posts/2022/Apr/22/Async-and-Async-Void-Event-Handling-in-WPF – Mario Vernari Apr 18 '23 at 07:54
  • well, I did use private async void Wait_FileSaved( ), is this wrong then? Better to return a Task? – michi.wtr Apr 18 '23 at 08:44
  • did you read the entire article? – Mario Vernari Apr 18 '23 at 09:33
  • 1
    Not a fan of [firing-and-forgetting](https://stackoverflow.com/questions/61316504/proper-way-to-start-and-async-fire-and-forget-call/61320933#61320933) the `Wait_FileSaved()` task. This task should be awaited. – Theodor Zoulias Apr 18 '23 at 11:03
  • @TheodorZoulias that's what I said. – Mario Vernari Apr 18 '23 at 11:39
  • Why don't you demonstrate correct code inside the answer? When evaluating answers, comments don't count. – Theodor Zoulias Apr 18 '23 at 12:04
  • @TheodorZoulias as written, I wanted to keep the snippet the simplest as possible in order the author can understand. Do you really think that's a bad habit? – Mario Vernari Apr 18 '23 at 12:26
  • Simplicity is good, but it shouldn't come at the cost of correctness. Your answer as it stands now teaches the OP bad habits (fire-and-forget), that they will later have to unlearn. – Theodor Zoulias Apr 18 '23 at 14:20
  • @TheodorZoulias what should be your answer, then? his code is not clear about event handlers (where the `async void` is tolerated) or not. `Task.Run` maybe? if so, that's another fire-and-forget. Honestly speaking, I wouldn't know what's the best answer for him. Could you help in that way? – Mario Vernari Apr 19 '23 at 04:02
  • I would suggest to change the return type of the `Open_PDF` from `void` to `Task`, `await` the `Wait_FileSaved()`, and let the OP figure out the rest (or wait for them to ask for further instructions). – Theodor Zoulias Apr 19 '23 at 04:40
  • @TheodorZoulias I could agree with you, but it would look the chicken-egg story. We well know that the async-y pattern is like a domino: no any easy well written solution to explain to a newbie. If you want, I'd have written the code in completely different way, for instance, checking for the `Process` end notification, as written earlier. – Mario Vernari Apr 19 '23 at 04:44
  • This is the nature of `async`/`await`. Since you decided to suggest it as a solution, you should demonstrate best practices. If you demonstrate fire-and-forget then, guess what, the OP will use fire-and-forget thinking that it's a decent option. Which you know that it's not. – Theodor Zoulias Apr 19 '23 at 04:52
  • @TheodorZoulias edited, just for completeness. However, you didn't answer me. The fire-and-forget is discouraged, but is the only thing you can do at the very top of a call stack or at the event handler level. So, is *not* true that you never have to use fire-and-forget: you should avoid in all the area where you can await it. Do you know it differently? – Mario Vernari Apr 19 '23 at 04:59
  • 1
    I guess that you consider `async void` to be fire-and-forget. This is a common misconception. Errors thrown in `async void` methods are rethrown on the `SynchronizationContext` that was captured when the `async void` started. `async void` is better described as [fire-and-crash](https://stackoverflow.com/questions/17659603/async-void-asp-net-and-count-of-outstanding-operations/17660475#17660475), because in application types without sync context (like Console apps) any error is simply crashing the process. Errors in `async void` methods are always surfaced, unlike fire-and-forget. – Theodor Zoulias Apr 19 '23 at 05:09
  • @TheodorZoulias finally I understand what you mean: all around exceptions. However, I would prefer to mark the `Open_PDF` with `async void` without further info from the OP. I'd assume that's the topmost call (i.e. an event handler), if you want, by clarifying so. Thanks for the useful chat. – Mario Vernari Apr 19 '23 at 06:09
  • [`async void`](https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming#avoid-async-void) is intended mainly for event handlers, and the `Open_PDF` doesn't look like an event handler. So demonstrating an `async void` method here would still be harmful in a different way IMHO. – Theodor Zoulias Apr 19 '23 at 06:15
  • @TheodorZoulias edited my answer. Hope is clear enough. – Mario Vernari Apr 19 '23 at 06:19
  • It is clear, but it is still more harmful than useful IMHO. It's your answer, and you don't have to change it according to my likings. I've just provided feedback and explained the reason for the -1 vote. – Theodor Zoulias Apr 19 '23 at 06:26