Background loading in MVVM and Constructor Injecti

2019-05-14 01:29发布

I have a question about how and where to load a large amount of data with ViewModel in WPF .NET 4.0 (so no async/await :/ ).

Here is my ViewModel:

public class PersonsViewModel : ViewModelBase
{
    private readonly IRepository<Person> _personRepository;

    private IEnumerable<Person> _persons;
    public IEnumerable<Person> Persons
    {
        get { return _persons; }
        private set { _persons = value; OnPropertyChanged("Persons"); }
    }

    public PersonsViewModel(IRepository<Person> personRepository)
    {
        if (personRepository == null)
            throw new ArgumentNullException("personRepository");

        _personRepository = personRepository;
    }
}

This ViewModel is used in a Window and I need to load all the persons when the Window opens. I thought of many solutions but I can't figure which is the best (or maybe there's a better way to do this). I have two contraints: - all the data must be loaded in another thread because it can take seconds to load (huge amount of data in the database) and I don't want to freeze the UI. - the ViewModel must be testable.

--=[ First solution: Lazy loading ]=--

public class PersonsViewModel : ViewModelBase
{
    private IEnumerable<Person> _persons;
    public IEnumerable<Person> Persons
    {
        get
        {
            if (_persons == null)
                _persons = _personRepository.GetAll();
            return _persons;
        }
    }
}

I don't like this solution because the data is loaded in the main thread.

--=[ Second solution: Loaded event ]=--

public class PersonsViewModel : ViewModelBase
{
    // ...

    private Boolean _isDataLoaded;
    public Boolean IsDataLoaded
    {
        get { return _isDataLoaded; }
        private set { _isDataLoaded = value; OnPropertyChanged("IsDataLoaded"); }
    }

    public void LoadDataAsync()
    {
        if(this.IsDataLoaded)
            return;

        var bwLoadData = new BackgroundWorker();
        bwLoadData.DoWork +=
            (sender, e) => e.Result = _personRepository.GetAll();
        bwLoadData.RunWorkerCompleted +=
            (sender, e) => 
            {
                this.Persons = (IEnumerable<Person>)e.Result;
                this.IsDataLoaded = true;
            };
        bwLoadData.RunWorkerAsync();
    }
}

public class PersonWindow : Window
{
    private readonly PersonsViewModel _personsViewModel;

    public PersonWindow(IRepository<Person> personRepository)
    {
        _personsViewModel = new PersonsViewModel(personRepository);

        this.Loaded += PersonWindow_Loaded;
    }

    private void PersonWindow_Loaded(Object sender, RoutedEventArgs e)
    {
        this.Loaded -= PersonWindow_Loaded;

        _personsViewModel.LoadDataAsync();
    }
}

I don't really like this solution because it forces the user of the ViewModel to call the LoadDataAsync method.

--=[ Third solution: load data in the ViewModel constructor ]=--

public class PersonsViewModel : ViewModelBase
{
    // ...

    public PersonsViewModel(IRepository<Person> personRepository)
    {
        if (personRepository == null)
            throw new ArgumentNullException("personRepository");

        _personRepository = personRepository;

        this.LoadDataAsync();
    }   

    private Boolean _isDataLoaded;
    public Boolean IsDataLoaded
    {
        get { return _isDataLoaded; }
        private set { _isDataLoaded = value; OnPropertyChanged("IsDataLoaded"); }
    }

    public void LoadDataAsync()
    {
        if(this.IsDataLoaded)
            return;

        var bwLoadData = new BackgroundWorker();
        bwLoadData.DoWork +=
            (sender, e) => e.Result = _personRepository.GetAll();
        bwLoadData.RunWorkerCompleted +=
            (sender, e) => 
            {
                this.Persons = (IEnumerable<Person>)e.Result;
                this.IsDataLoaded = true;
            };
        bwLoadData.RunWorkerAsync();
    }
}

In this solution, the user of the ViewModel don't need to call an extra method to load data, but it violates the Single Responsability Principle as Mark Seeman says in his book "Dependency Injection" : "Keep the constructor free of any logic. The SRP implies that members should do only one thing, and now that we use the constructor to inject DEPENDENCIES, we should prefer to keep it free of other concerns".

Any ideas to resolve this problem in a proper way?

1条回答
孤傲高冷的网名
2楼-- · 2019-05-14 01:58

Difficult to give an accurate answer without knowing how you tie your ViewModels to your Views.

One practice is to have a "navigation aware" ViewModel (a ViewModel which implements a certain interface like INavigationAware and have your navigation service call this method when it instantiates the ViewModel/View and tie them together. It's the way how Prism's FrameNavigationService works.

i.e.

public interface INavigationAware
{
    Task NavigatedTo(object param);
    Task NavigatedFrom(...)
}

public class PersonWindow : ViewModelBase, INavigationAware 
{

}

and implement your initialization code within NavigatedTo which would be called from the navigation service, if the ViewModel implements INavigationAware.

Prism for Windows Store Apps References:

查看更多
登录 后发表回答