-1

I have a View Model where

protected override void Initialize()
{
    this.InnerViewModel.LoadInformation();

    this.IsInformationLoaded = true;
}

however, this.InnerViewModel.LoadInformation(); is very CPU and IO intensive operation. It may take a few seconds to complete.

The View of that model has binding to IsInformationLoaded to show a 'Loading...' screen.

My current implementation, as you can imagine, doesn't show the 'Loading...' screen, instead the UI is frozen until the operation is complete.

How do I change change the Initialize method, to make the LoadInformation asynchronous? Please bear in mind that InnerViewModel is also View-bound.

Edit:

The following works

protected override async void Initialize()
{
    await Task.Run(() =>
    {
        this.InnerViewModel.LoadInformation();
    });

    this.IsInformationLoaded = true;
}

// In InnerViewModel
public override void LoadInformation()
{
    Thread.Sleep(3000);

    Application.Current.Dispatcher.Invoke(() =>
    {
        this.SomethingObservable.Clear();
        this.SomethingObservable.Add(...something ...);
    });
}

however I really want to get rid of Application.Current.Dispatcher, if possible. Or somehow move it into the Initialize(). I don't want the InnerModel to know anything about how it's being executed.

hyankov
  • 4,049
  • 1
  • 29
  • 46
  • What happens if you set `IsInformationLoaded = false` in the method, then `Task.Run(this.InnerViewModel.LoadInformation).ContinueWith(() => {this.IsInformationLoaded = true;});` ? If you set the loaded boolean to on the next line after Task.Run, it'll execute the line before the task is ready. – cbr Jan 09 '17 at 14:51
  • If that wasn't the issue, could you perhaps clarify what you mean when you say "`Task.Run()` and similar approaches [were not successful]" – cbr Jan 09 '17 at 14:52
  • I have edited by question @cubrr – hyankov Jan 09 '17 at 14:58
  • 1
    For your newly discovered threading issue, you'll want to see [this question](http://stackoverflow.com/questions/10450750/can-you-access-ui-elements-from-another-thread-get-not-set). – cbr Jan 09 '17 at 15:02
  • This may work but then `InnerViewModel` will have knowledge that it is being executed in async fashion, which I'd love to avoid if I can ... – hyankov Jan 09 '17 at 15:04
  • Why? You can't run something on a different thread and prohibit that method from finding out it's running on a different thread. Perhaps your issue is that you're doing too much in the method - both doing the work required to gather information _and_ setting the UI. – cbr Jan 09 '17 at 15:06
  • Well, for instance I may decide to reuse the same component on a different place, not executed in a `Task`. – hyankov Jan 09 '17 at 15:10

1 Answers1

0

Also, it would be best if InnerViewModel's LoadInformation doesn't know that it is being executed asynchronously and/or doesn't do any special handling about it. Is that possible?

In .NET Framework 4.5+ you can enable access to a data bound collection from a background thread by calling the BindingOperations.EnableCollectionSynchronization method, e.g.:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = new WindowViewModel();
    }
}

public class WindowViewModel
{
    private readonly object _lock = new object();

    public ObservableCollection<string> Items { get; } = new ObservableCollection<string>();

    public WindowViewModel()
    {
        BindingOperations.EnableCollectionSynchronization(Items, _lock);
        Task.Run(() =>
        {
            for (int i = 0; i < 100; ++i)
            {
                Items.Add(i.ToString());
                Thread.Sleep(2000);
            }
        });
    }
}

<ListBox ItemsSource="{Binding Items}"></ListBox>

The other option is to use the dispatcher to marshal the operations that access the data bound collection back to the UI thread:

Task.Run(() =>
            {
                for (int i = 0; i< 100; ++i)
                {
                    Application.Current.Dispatcher.Invoke(new Action(() => Items.Add(i.ToString())));
                    Thread.Sleep(2000);
                }
            });

This kind of makes the method aware of the fact that it is indeed executing on a background thread though.

mm8
  • 163,881
  • 10
  • 57
  • 88
  • but `BindingOperations.EnableCollectionSynchronization` doesn't work if I do it from within the method which is executed in a `Task`. – hyankov Jan 09 '17 at 17:42
  • Call it on the UI thread before starting the background thread as per my example. Anyway, it's either this or using the dispatcher. – mm8 Jan 09 '17 at 17:48