0

I'm currently trying to write a simple C#-WPF-Application that functions as a simple universal 'launcher'. For different applications we program.

It's purpose is to check the current software version of the 'real' software and if a new one is available it starts to copy the installer from a network share and runs the installer afterwards. Then it starts the 'real' application and thats it.

The user Interface mainly consists of a startup window which shows the user the currently executed action (version check, copy, installation, startup, ...).

Now I create my view and my viewModel in the overridden StartUp method in App.cs

public override OnStartup(string[] args)
{
    var viewModel = new StartViewModel();
    var view = new StartView();

    view.DataContext = viewModel;

    view.Show();

    // HERE the logic for the Launch starts
    Task.Run(() => Launch.Run(args));
}

The problem is that if I don't go async here the Main Thread is blocked and I cannot update the UI. Therefore I got it working by using the Task.Run(...). This solves my problem of blocking the UI thread, but I have some problems/questions with this:

  1. I cannot await the task, because that would block the UI again. Where to await it?
  2. Is my concept of starting this workflow here ok in the first place?

Some update to clarify: After I show the UI to the user my logic starts to do heavy IO stuff. The possible calls I came up with are the following 3 variants:

view.Show();

// v1: completely blocks the UI, exceptions are caught
DoHeavyIOWork();

// v2: doesn't block the UI, but exceptions aren't caught
Task.Run(() => DoHeavyIOWork());

// v3: doesn't block the UI, exceptions are caught
await Task.Run(() => DoHeavyIOWork());

Currently I'm not at my work PC so i apologies for not giving you the original code. This is an on the fly created version.

I guess v1 and v2 are bad because of exceptions and the UI blocking.

I thought v3 didn't work when I tried it in my office. Now it seems to work in my local example. But I'm really not sure about v3. Because I'm using async void StartUp(...) there. Is it okay here?

  • 1
    I usually make a View ctor with the ViewModel parameter, instead setting DataContext outside the View class. Furthermore, I'll run the task before you make 'view.Show()' and I don't call 'view.Show()', I call 'Run(view)'. But yeah, looks fine (at least for me) – DrkDeveloper Jul 11 '21 at 12:49
  • 2
    Why can't you `await` the `Task`? What do you mean it `would block the UI again`? The `Task.Run()` in your code is queuing up a new `Thread` on the threadpool and executing it in a fire-and-forget fashion. Your code does not wait for `Launch.Run()` to complete since you're not `await`ing the `Task.Run()`. This could cause problems if `Launch.Run()` is supposed to be done before the code after `OnStartup()` executes. – Patrick Tucci Jul 11 '21 at 13:00
  • 1
    It seems that you are trying to reinvent the [ClickOnce](https://en.wikipedia.org/wiki/ClickOnce) technology: *"ClickOnce applications can be self-updating. They can check for newer versions as they become available and automatically replace any updated files. Depending on the installation type, ClickOnce presents several update options. Applications can be configured to check for updates on startup or after startup."* [Deploy a .NET Windows desktop application using ClickOnce](https://learn.microsoft.com/en-us/visualstudio/deployment/quickstart-deploy-using-clickonce-folder) – Theodor Zoulias Jul 11 '21 at 13:11
  • Regarding 'ClickOnce': In my setting we have a number of machines running our software but it is used in an production setting where the user can and should not accept or decline the installation. Also different machines sometimes have to run different versions of the software. I just don't know if ClickOnce is the best choice there. – FlyingHubert Jul 11 '21 at 13:21
  • @DrkDeveloper I get your points. Even if off topic: Why is Run(window) better than window.Show()? – FlyingHubert Jul 11 '21 at 13:48
  • @PatrickTucci Thanks for your reply. I tried to clarify my question by editing it. There is no code after the startup. It's just a sequential flow of tasks which also tries to Notify the user by presenting some Info in the UI. No Buttons, no user interaction. Could even be a console application :) But this isn't a good idea for the users really using the software. – FlyingHubert Jul 11 '21 at 14:00
  • I think wpf is not ideal for this sort of app. – Andy Jul 11 '21 at 14:09
  • Have you considered using a windows service to do the copying? This could start checking at windows start up and again say every hour. – Andy Jul 11 '21 at 14:11
  • await the Task in an async Loaded event handler of the Window. – Clemens Jul 11 '21 at 15:14
  • @Andy tbh. I haven't thought about that. After doing some searches on google that might be an option. But I have to look if it is as simple as deploying this wpf app. The problem is, that I'm do not have the same rights/users on all machines. That might be an issue. – FlyingHubert Jul 11 '21 at 15:36
  • @Clemens Like I did in my third version but moving this call to the OnLoaded event? – FlyingHubert Jul 11 '21 at 15:37
  • There is no OnLoaded event, but `Loaded`. You could attach an async event handler method. – Clemens Jul 11 '21 at 17:26
  • 1
    @FlyingHubert https://stackoverflow.com/questions/20859048/whats-the-difference-between-show-showdialog-and-application-run-functio – DrkDeveloper Jul 11 '21 at 19:43

2 Answers2

2

I cannot await the task, because that would block the UI again. Where to await it?

await doesn't block the UI. Using await here is fine.

Is my concept of starting this workflow here ok in the first place?

I usually recommend that some UI is shown immediately when doing any asynchronous operation. Then when the async operation is complete, you can update/replace the UI.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
0

Thanks for all the replys. After reading all your comments and combining some of your answers I came up with the following example. It is working under all circumstances I tested.

Hopefully there is not to much wrong in your opinion.

Code behind from App.xaml.cs

public partial class App : Application
{
    readonly StartViewModel viewModel = new StartViewModel();

    protected override async void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        var view = new StartWindow
        {
            DataContext = viewModel
        };

        view.Show(); // alternative:  Run(view);

        // alternative: instead of calling it here it is possible
        //              to move these calls as callback into the Loaded of the view.
        await Task.Run(() => DoHeavyIOWork()); 
    }
        
    private string GenerateContent()
    {
        var content = new StringBuilder(1024 * 1024 * 100); // Just an example.
        for (var i = 0; i < 1024 * 1024 * 2; i++)            
            content.Append("01234567890123456789012345678901234567890123456789");

        return content.ToString();
    }

    private void DoHeavyIOWork()
    {
        var file = Path.GetTempFileName();

        for (var i = 0; i < 20; i++)
        {
            File.WriteAllText(file, GenerateContent());
            File.Delete(file);

            Dispatcher.Invoke(() => viewModel.Info = $"Executed {i} times.");
        }
    }
}

Code in StartViewModel.cs

class StartViewModel : INotifyPropertyChanged
{
    private string info;

    public event PropertyChangedEventHandler PropertyChanged;

    public string Info
    {
        get => info; 
        set
        {
            info = value;
            OnPropertyChanged();
        }
    }

    private void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}