robin robin - 2 months ago 32
C# Question

Binding data to child views in WPF (MVVM)

I'm trying to build an application with WPF and MVVM protocol. The application have one windows and multiple views (UserControls). One of the views also have child views inside to show different data.

My problem
I can not understand how to bind data down to the child views. I've tried to understand how the data is bound and but have only manage to bind the data one level down.

Code

Here are some of the code. I hope it make it easier to understand my problem.

App.xaml.cs

public partial class App : Application
{

private Engine _engine;

protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
MainWindow window = new MainWindow();
_engine = new Engine("test");

var viewModel = new MainWindowViewModel(_engine);

EventHandler handler = null;
handler = delegate
{
viewModel.RequestClose -= handler;
window.Close();
};
viewModel.RequestClose += handler;
window.DataContext = viewModel;

window.Show();
}
}


Here is where I create the engine object and pass it down to the MainWindowViewModel that I want to further bind down the view hierarchy.

MainWindow.xaml

<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:"
xmlns:ViewModels="clr-namespace:ViewModels"
xmlns:View="clr-namespace:Views"
x:Class="MainWindow"
Title="title" Height="800" Width="1200"
WindowStartupLocation="CenterScreen" Icon="Resources/Images/logo.png"
>

<DockPanel Margin="0" Background="#FF4F4F4F" LastChildFill="True">
<Menu DockPanel.Dock="Top" Height="20">
<MenuItem Header="File">
<MenuItem Header="Exit"/>
</MenuItem>
</Menu>

<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" MinWidth="200" MaxWidth="200"/>
<ColumnDefinition Width="5*"/>
</Grid.ColumnDefinitions>

<View:TabView Grid.Column="0"/>

<View:WorkspaceView Grid.Column="1"/>
</Grid>
</DockPanel>




WorkspaceView.xaml

<UserControl x:Class="Views.WorkspaceView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Views"
xmlns:ViewModels="clr-namespace:ViewModels"
mc:Ignorable="d"
d:DesignHeight="750"
d:DesignWidth="1000"
d:DataContext="{d:DesignInstance ViewModels:WorkspaceViewModel}"
>

<Grid>
<Label x:Name="label" Height="750" VerticalAlignment="Top" FontSize="60" Foreground="White" Content="{Binding Engine.BarCode}"/>
<Grid Margin="40">
<Grid.Background>
<ImageBrush ImageSource="/;component/Resources/Images/logo.png" Stretch="Uniform"/>
</Grid.Background>
<ContentPresenter Content="{Binding CurrentView}"/>
</Grid>

<DockPanel>

<!--ContentPresenter Content="{Binding CurrentView}"/-->
</DockPanel>
</Grid>




Here I'm trying to bind
{Binding Engine.BarCode}
which works and gives me a string with the correct data. But
<ContentPresenter Content="{Binding CurrentView}"/>
won't show the current view that I'm setting in the ViewModel for the workspaceView.

WorkspaceViewModel.cs

public WorkspaceViewModel()
{
_currentView = new InjectorView();

}
public UserControl CurrentView
{
get { return _currentView; }
}



  • Update:
    _currentView = new InjectorView();
    is just for test and the currentView should change depending on buttons pressed in another view.*



WorkspaceView.xaml.cs

public WorkspaceView()
{
InitializeComponent();
this.DataContext = new WorkspaceViewModel();
}


InjectorView.xaml

<UserControl x:Class="Views.InjectorView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Views"
xmlns:ViewModels="clr-namespace:ViewModels"
mc:Ignorable="d"
d:DesignHeight="750"
d:DesignWidth="1000"
d:DataContext="{d:DesignInstance ViewModels:InjectorViewModel}"
>
<Grid Background="#FFAEAEAE">
<Label x:Name="label1" Content="{Binding Engine.BarCode}"/>
</Grid>
</UserControl>


But if I remove
d:DataContext="{d:DesignInstance ViewModels:WorkspaceViewModel}"
from WorkspaceView.xaml and add
this.DataContext = new WorkspaceViewModel();
to the c# file for the xaml code it will show the current view (InjectorView). The only problem now is when I try to bind some data in InjectorView
{Binding Engine.BarCode}
it won't show the same string as before (I guess it's not the same instance of the object anymore??)

What am I missing? Have I interpret MVVM and wpf completly wrong?

(I had to remove some code (for example namespaces) due to the product)

Jai Jai
Answer

You definitely misunderstood quite a fair bit.

Firstly, _currentView = new InjectorView(); should never appear in ViewModel. ViewModels are not supposed to hold any reference of any visual classes (your own View classes and UI classes). You should instantiate the ViewModel for that View.

Next, if that CurrentView is always an instance of InjectorView (which means it can't be something else), then you can simply do this:

<View:InjectorView>
    <View:InjectorView.DataContext>
        <ViewModel:InjectorViewModel />
    <View:InjectorView.DataContext>
</View:InjectorView>

This is very similar to what you have done in MainWindow.xaml for WorkspaceView.

Now to the next big problem. d:DataContext="{d:DesignInstance ViewModels:InjectorViewModel}" is used as a design-time DataContext. What this means is that, normally without this, the Visual Studios Designer would not be render data-bound things, because at design-time there is no DataContext. That line tells the designer that you want to create an instance of InjectorViewModel to simulate this behavior - but this is purely for the designer. When your applications runs, that d:DataContext line has absolutely no impact on your application.

At the moment, your InjectorView has no ViewModel (no DataContext). If you follow what I suggested in earlier part of this answer, then you would have a DataContext now.

Edit (based on OP's comment)

The method above is called View First Approach. Using this approach, you define a View instance in your XAML. You then attach the corresponding ViewModel using DataContext.

For your case, you should use the ViewModel First Approach. You define what components to use in your ViewModel.

WorkspaceViewModel:

private ViewModelBase _myCurrentView;
public ViewModelBase MyCurrentView
{
    get { return _currentView; }
    set
    {
        if (value != _myCurrentView)
        {
            _myCurrentView = value;
            RaisePropertyChanged(); // You need to implement INotifyPropertyChanged interface
        }
    }
}

WorkspaceView:

<ContentControl Content="{Binding MyCurrentView}">
    <ContentControl.Resources>
        <DataTemplate DataType="{x:Type ViewModels:InjectorViewModel}">
            <local:InjectorView />
        </DataTemplate>
        <DataTemplate DataType="{x:Type ViewModels:MySecondViewModel}">
            <local:MySecondView />
        </DataTemplate>
        .....
        ....
    </ContentControl.Resources>
</ContentControl>

Using this method, when you need to load an InjectorView, you would simply instantiate its ViewModel InjectorViewModel, and assign it to MyCurrentView property.

In your WorkspaceView View, you would use a ContentControl to host this child View. This ContentControl needs to bind to the MyCurrentView property, which is of ViewModelBase type. The DataTemplate will tell WPF that if the content is of InjectorViewModel type, then instantiate an InjectorView object for me, as InjectorViewModel is just a non-visual data object - the renderable equivalent is InjectorView. You need to create a DataTemplate for each of the possible ViewModel classes that you are expecting.

Two things to note when you use this approach (ViewModel First). Firstly, all the ViewModels that can be dynamically loaded must be subclasses of ViewModelBase. ViewModelBase can be an abstract class, or an interface, and you are free to change to whichever name you want. The most important thing is that it must be the common class/interface for all the possible ViewModels.

Secondly, you do not need to instantiate another ViewModel in your View. You also do not need to set DataContext. Using DataTemplate will automatically sets the DataContext of the View.

Edit 2

There are many ways to pass data from main ViewModel to sub ViewModels. One way is to have a repository (database) hold the data.

If you do not want to have a repository, you can either use a singleton to emulator as a repository, or have the main ViewModel hold all data, and have all other ViewModel hold a reference of the main ViewModel instance. But honestly, this is harder to implement when your sub ViewModels are instantiated by View-First Approach, because ViewModels are instantiated by View, which will not call the constructor passing in the main ViewModel.

Another way to overcome this is to create singleton ViewModels, with static internal singleton instance. This allow the ViewModels to access one another's data. To make ViewModels singleton, you need to change how View-First Approach define the DataContext.

For example, using the same example I provided for View-First InjectorView earlier:

<View:InjectorView>
    <View:InjectorView.DataContext>
        <Binding Source="{x:Static ViewModel:InjectorViewModel.Instance}" />
    <View:InjectorView.DataContext>
</View:InjectorView>

You cannot instantiate the ViewModel from View because you would have to make the constructor private for singleton implementation. You would instead provide the binding to the static instance.

Overall, I would just use a singleton class to act like a repository. It seems easy for me to implement.

Comments