Pergunta

Often when I’m designing an MVVM application, the following scenario comes up. A Window, having multiple children, sharing the same data source.

I’m trying to decide on the best way to implement a single datasource for all children. There are 3 options I can think of, all with their own advantages and disadvantages.

Example

The Window has two child UserControls, each in their own tab.

Main Window Example

UI is linked up like this.

XAML Design

In order to keep modularity and provide them with data, the same design is reflected in ViewModels.

ViewModel Design

The MainViewModel is set up like this.

public class MainViewModel : ViewModelBase
{
    private readonly ChildViewModelA _childViewModelA = new ChildViewModelA();
    private readonly ChildViewModelB _childViewModelB = new ChildViewModelB();

    public ChildViewModelA ChildViewModelA { get { return this._childViewModelA; } }
    public ChildViewModelB ChildViewModelB { get { return this._childViewModelB; } }
}

The MainWindow instantiates the MainViewModel and sets DataContext of the children. Child controls are bound to data properties.

public partial class MainWindow : Window
{
    private readonly MainViewModel _viewModel = new MainViewModel();
    
    public MainWindow()
    {
        InitializeComponent();
    }

    public MainViewModel ViewModel { get { return this._viewModel; } }
}

<Window x:Class="WpfApplication3.View.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:controls="clr-namespace:WpfApplication3.View.Controls"
    Title="MainWindow" Height="350" Width="525"
    DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid>
    <TabControl>
        <TabItem Header="Tab 1">
            <controls:ChildViewA DataContext="{Binding ViewModel.ChildViewModelA}"/>
        </TabItem>
        <TabItem Header="Tab 2">
            <controls:ChildViewB DataContext="{Binding ViewModel.ChildViewModelB}"/>
        </TabItem>
    </TabControl>
</Grid>
</Window>

To prevent every ViewModel from retrieving the same data from the database, I want to load data in the MainViewModel and provide the children. However, there are multiple ways of doing this.

Example 1: Using a setter method on the children

public class MainViewModel : ViewModelBase
{
    ...
    private readonly FakeDataManager _fakeDataManager = new FakeDataManager();
    public MainViewModel()
    {
        this.CurrentPerson = _fakeDataManager.GetNextPerson();
    }

    private Person _currentPerson;

    public Person CurrentPerson
    {
        get { return this._currentPerson; }
        set
        {
            this._currentPerson = value;
            this.RaisePropertyChanged("CurrentPerson");

            this.ChildViewModelA.SetPerson(this.CurrentPerson);
            this.ChildViewModelB.SetPerson(this.CurrentPerson);
        }
    }


public class ChildViewModelA : ViewModelBase
{
    private Person _currentPerson;

    public Person CurrentPerson
    {
        get { return this._currentPerson; }
        set
        {
            this._currentPerson = value;
            this.RaisePropertyChanged("CurrentPerson");
        }
    }
}

Easy to implement, however quickly get's hard to remain. Not a lot of code reuse. No loose coupling. Should not use this.

Example 2: Putting data in a container

public class MainViewDataContainer : INotifyPropertyChanged
{
    private Person _currentPerson;

    public Person CurrentPerson
    {
        get { return this._currentPerson; }
        set
        {
            this._currentPerson = value;
            this.RaisePropertyChanged("CurrentPerson");
        }
    }
}

public class MainViewModel : ViewModelBase
{
    ...
    private readonly FakeDataManager _fakeDataManager = new FakeDataManager();
    private readonly MainViewDataContainer _dataContainer = new MainViewDataContainer();

    public MainViewModel()
    {
        this._childViewModelA = new ChildViewModelA(_dataContainer);
        this._childViewModelB = new ChildViewModelB(_dataContainer);

        this._dataContainer.CurrentPerson = _fakeDataManager.GetNextPerson();
    }

public class ChildViewModelA : ViewModelBase
{
    private readonly MainViewDataContainer _dataContainer;

    public ChildViewModelA(MainViewDataContainer dataContainer)
    {
        this._dataContainer = dataContainer;
    }

    public MainViewDataContainer DataContainer { get { return this._dataContainer; } }
}

Easier to maintain, more code reuse. Looser coupling.

Example 3: Storing in MainViewModel Properties and provide to children through an interface

public interface IMainDataProvider
{
    Person CurrentPerson { get; }
}

public class MainViewModel : ViewModelBase, IMainDataProvider
{
    private readonly ChildViewModelA _childViewModelA;
    private readonly ChildViewModelB _childViewModelB;

    private readonly FakeDataManager _fakeDataManager = new FakeDataManager();

    public MainViewModel()
    {
        this._childViewModelA = new ChildViewModelA(this);
        this._childViewModelB = new ChildViewModelB(this);

        this.CurrentPerson = _fakeDataManager.GetNextPerson();
    }

    private Person _currentPerson;

    public Person CurrentPerson
    {
        get { return this._currentPerson; }
        set
        {
            this._currentPerson = value;
            this.RaisePropertyChanged("CurrentPerson");
        }
    }
}

public class ChildViewModelA : ViewModelBase
{
    private readonly IMainDataProvider _dataProvider;

    public ChildViewModelA(IMainDataProvider dataProvider)
    {
        this._dataProvider = dataProvider;
    }

    public IMainDataProvider DataProvider { get { return this._dataProvider; } }
}

Yet again easier to maintain, more code reuse. Loose coupling.

Example 3 seems to be the best solution, however is this true? How do you think about this? Are there better ways to solve this issue?

Foi útil?

Solução

If CurrentPerson is used by both ChildViewModels and MainView is the one who can change the CurrentPerson. How I will proceed to solve this problem is:

  1. Define one base VM ChildViewModelBase : ViewModelBase with CurrentPerson property (with INotifyPropertyChanged and all). All of my ChildViewModels will extend this VM.

  2. Use Event Aggregator http://msdn.microsoft.com/en-us/library/ff921122.aspx. Using EventAggregator you can subscribe to CurrentPersonChanged event in your ChildViewModelBase.

  3. From you MainViewModel once the CurrentPerson is loaded from db, raise CurrentPersonChanged event with new Person as event arguement.

  4. In event handler in the ChildViewModelBase, set CurrentPerson that you get as event argument.

In this way it would not matter to your ChildViewModels who is loading the Person from DB. Today it is MainView, tommorow it can be some other View also. You will just need to raise the above event with the person object as argument and Child Views will get it.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top