consoleframework icon indicating copy to clipboard operation
consoleframework copied to clipboard

Scheduling tasks before `ConsoleApplication` has been started

Open ForNeVeR opened this issue 4 years ago • 3 comments

I have a couple of view models that start to execute some asynchronous tasks in their constructors (or as part of their initialization). For an example, let's consider something like this:

MyWindow.xaml:

<c:Window xmlns:c="clr-namespace:ConsoleFramework.Controls;assembly=ConsoleFramework"
          xmlns:x="clr-namespace:ConsoleFramework.Xaml;assembly=ConsoleFramework"
          xmlns:controls="clr-namespace:Fenrir.Ui.Framework.Controls;assembly=Fenrir.Ui.Framework"
          MinHeight="10" MinWidth="25"
          X="0" Y="0"
          Title="Wnd">
    <c:ListBox VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
               Items="{x:Binding ItemList, Mode=OneWay}" />
</c:Window>

Program.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Binding.Observables;
using ConsoleFramework;
using ConsoleFramework.Controls;

namespace ConsoleApp2
{
    public class MyViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        public ObservableList<string> ItemList { get; } = new ObservableList<string>(new List<string>());

        public MyViewModel()
        {
            // Load initial data
            Task.Run(async () =>
            {
                ConsoleApplication.Instance.Post(() =>
                {
                    ItemList.Add("a");
                    ItemList.Add("b");
                    ItemList.Add("c");
                });
            });
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var host = new WindowsHost();
            var window = (Window) ConsoleApplication.LoadFromXaml(
                "ConsoleApp2.MyWindow.xaml",
                new MyViewModel());
            host.Show(window);
            ConsoleApplication.Instance.Run(host);
        }
    }
}

Here, I have a data source that's populated asynchronously at the application start. It may load data from network or whatever, and then delegates the UI-related task to UI thread, in a classic fashion.

The problem is that it doesn't work: most of the time, at the moment I call ConsoleApplication.Instance.Post, the console application hasn't started yet, and thus it rejects my task: https://github.com/elw00d/consoleframework/blob/e6300d203620202e1443dd96687e45ba3242fdf1/ConsoleFramework/ConsoleApplication.cs#L977-L981

How do I solve this problem?

I've tried to lazily start the task in the ItemList accessor:

        private ObservableList<string> _itemList;
        public ObservableList<string> ItemList
        {
            get
            {
                if (_itemList == null)
                {
                    _itemList = new ObservableList<string>(new List<string>());
                    // Load initial data
                    Task.Run(async () =>
                    {
                        ConsoleApplication.Instance.Post(() =>
                        {
                            ItemList.Add("a");
                            ItemList.Add("b");
                            ItemList.Add("c");
                        });
                    });
                }
                 return _itemList;
            }
        }

This doesn't work because the binding calls the accessor before the application hass been started, in the LoadFromXaml.

I haven't found any reliable way to inject my code into the application event queue, except a very hacky one. Since I know that the application will activate every window I Show, I could do something like this:

var viewModel = new MyViewModel();
var window = (Window) ConsoleApplication.LoadFromXaml(
    "ConsoleApp2.MyWindow.xaml",
    viewModel);
host.Show(window);
EventHandler oneTimeHandler = null;
oneTimeHandler = (_, __) =>
{
    Task.Run(async () =>
    {
        ConsoleApplication.Instance.Post(() =>
        {
            viewModel.ItemList.Add("a");
            viewModel.ItemList.Add("b");
            viewModel.ItemList.Add("c");
        });
    });
    EventManager.RemoveHandler(window, Window.ActivatedEvent, oneTimeHandler);
};

EventManager.AddHandler(window, Window.ActivatedEvent, oneTimeHandler);

This is, obviously, very hacky and uncool (but it works though).

If only ConsoleApplication had a way to schedule task before it has been started, or a legal way to know when it has been started… That way, I could wait for an event or whatever in my custom synchronization context routine.

So, here's my suggestion:

  1. Make ConsoleApplication.RunOnUiThread to return a bool, so the caller could know when their action was rejected from running by the application. Or even throw an exception.
  2. Perform the same change with ConsoleApplication.Post(Action action)
  3. Add an overload ConsoleApplication.Post(Action action, bool evenIfNotStarted). If evenIfNotStarted= true, then the application should always queue the action, even if it isn't running yet.
  4. Since the user now may try calling Post(smth, evenIfNotStarted: true) even after Dispose, add a guard: all the queue methods should throw exceptions if called after Dispose.
  5. Since it'll be possible to create a memory leak by adding queue methods and then calling Dispose without starting the application (which is theoretically possible even before the proposed changes), Dispose should empty the action queue.
  6. Just as a quality-of-life design improvement, I suggest Post(Action action, TimeSpan delay) to take an additional (optional) callback argument that will be called in case the action was rejected (either in case the application wasn't running or it was disposed at the time the callback has been dispatched).
  7. Make a public member of private volatile bool running, so the callers will be able to insect the application state.

@elw00d, do these changes look okay to you? I'm ready to send a PR with these improvements and a decent test suite. I believe it may be done without breaking the source compatibility (and I may work on binary compatibility, too, but I don't think it's necessary at this point though).

ForNeVeR avatar May 11 '20 16:05 ForNeVeR

Hi !

I think it will be a good solution. Wouldn't it be easier to remove this check at all ?

https://github.com/elw00d/consoleframework/blob/e6300d203620202e1443dd96687e45ba3242fdf1/ConsoleFramework/ConsoleApplication.cs#L977-L981

(with emptying the queue in Dispose, as you suggest)

elw00d avatar May 14 '20 16:05 elw00d

I belive we should reject any tasks to be put into the queue after Dispose, so I'd stick with the current approach.

It would be much easier to "just" remove the check, but I'd like to provide improved diagnostics to the library user in case they're doing something strange.

ForNeVeR avatar May 16 '20 10:05 ForNeVeR

Ok, let's take your plan )

elw00d avatar May 17 '20 10:05 elw00d