Nestor
Nestor

Reputation: 8384

How to avoid calling long operations from constructor

I use MVVM and I have to create a ViewModel class that should load lots of data when the View is opened.

Basically, when I create the viewmodel, it should use the database and get the data.

I used this approach first:

public class MainViewModel
{
 public MainViewModel()
 {
   //set some properties
   Title = "Main view";
   //collect the data
   StartLongOperation();
 }

 private void StartLongOperation()
 {
   Thread t=new Thread(...);
   t.start();
 }
}

It works and loads the data without blocking the UI thread.

Later I found this guideline about how to use constructor, and it does not recommend starting long operation from constructor.

√ DO minimal work in the constructor.

Constructors should not do much work other than capture the constructor parameters. The cost of any other processing should be delayed until required.

In my case, the data is required on opening the view.

My first idea was to use an event.

How should I avoid calling the long operation from construcTor? What is the best practice?

Upvotes: 1

Views: 1354

Answers (5)

Bondaryuk Vladimir
Bondaryuk Vladimir

Reputation: 519

I don't know maybe it's wrong but sometimes i make(if i need retrun parameters)

public class MainViewModel
    {
        public MainViewModel()
        {
            //set some properties
            Title = "Main view";
        }
        public static string GetMainViewModelString()
        {
            var mainViewModel = new MainViewModel();
            return mainViewModel.GetString();
        }
        public string GetString()
        {
            /*your code*/
        }
    }

and then call

var stringData = MainViewModel.GetMainViewModelString();

but when it need i call some operation from constructor

Upvotes: 0

Ewan
Ewan

Reputation: 1285

Maybe use a factory pattern to avoid having the MainViewModel around but not populated.

    public class VMFactory
    {
        public async Task<MainViewModel> GetVM()
        {
            MainViewModel vm = new MainViewModel();
            await vm.LongOperation();
            return vm;
        }
    }

    public class MainViewModel
    {
        public MainViewModel()
        {
            //set some properties
            Title = "Main view";
        }

        public async Task LongOperation()
        {
            (...)
        }
    }

or perhapse better. move the long running method out of the MainViewModel to a repository or service

    public class VMRepository
    {
        public async Task LongOperation()
        {
            (...)
        }

        public async Task<MainViewModel> GetVM()
        {
            MainViewModel vm = new MainViewModel();
            vm.DataWhichTakesAlongTime  = await LongOperation();
            return vm;
        }
    }

    public class MainViewModel
    {
        public MainViewModel()
        {
            //set some properties
            Title = "Main view";
        }

        public object DataWhichTakesAlongTime { get; set; }
    }

To be honest though it sounds from the conversations around this question that you are simply using the constructor as a convenient trigger for a 'LoadDataNow' command and really you should add an ICommand, bind it to something in the view (Loaded) add loading spinners and completed events etc etc

Controversially I might also suggest you add a Controller Class to instantiate the repository view and vm and call the 'LoadData' method on the view. Not very MVVM I know but essentially doing the same stuff your IoC container does without having to jump through the hoops of configuration

Upvotes: 1

ManOVision
ManOVision

Reputation: 1893

Use your view lifecycle to execute this method. You can use Tasks to simplify the execution and you can bind to other properties to show progress. Example shown using a Windows Store App view.

ViewModel:

public class MainViewModel
{
    public MainViewModel()
    {
        this.Title = "Main view";
    }

    public async Task StartLongOperationAsync()
    {
        this.IsLoading = true;

        await Task.Run(() =>
        {
            //do work here
        });

        this.IsLoading = false;
    }
}

And on the View:

protected override async void OnNavigatedTo(NavigationEventArgs e)
{
    base.OnNavigatedTo(e);

    await ((MainViewModel)this.DataContext).StartLongOperationAsync();
}

Upvotes: 0

Stephen Wilson
Stephen Wilson

Reputation: 1514

Miguel Castro has talked about a solution to this in one of his great Pluralsight courses. He binds to a property in the viewmodel called ViewLoaded which will obviously get bound when the view loads, this in turn will call your long running method.

So this goes in the view (Or a base class for all views to help with re-use):

    public ViewBase()
    {
        // Programmatically bind the view-model's ViewLoaded property to the view's ViewLoaded property.
        BindingOperations.SetBinding(this, ViewLoadedProperty, new Binding("ViewLoaded"));
    }

    public static readonly DependencyProperty ViewLoadedProperty =
        DependencyProperty.Register("ViewLoaded", typeof(object), typeof(UserControlViewBase),
        new PropertyMetadata(null));

And this is the ViewModel base class code:

public object ViewLoaded
{
    get
    {
        OnViewLoaded();
        return null;
    }
}

protected virtual void OnViewLoaded() { }

Simply override the OnViewLoaded() method in your ViewModel and call the long running method from there.

Upvotes: 5

James
James

Reputation: 9975

Avoiding calling it is simple, just split it into 2 methods; The constructor and a GetData method you call when you open the view or after you set the data context.

The why is just about managing expectation. If you hadn't written the code and were writing a new view for someone else's view model, would you expect the constructor to start accessing a database? or would you expect it just to construct a view model and you need to make a second call to initiate getting the data?

Upvotes: 0

Related Questions