Reputation: 616
This my first attempt at implementing the MVVM model so apologies in advance. I have a main window which has its own ViewModel. The Main Window then has three user controls which i which loads depending on what selected in the navigation menu. However the data context is not changing to the correct view model.
MainWindow.xaml
<Window.DataContext>
<VM:MainVM></VM:MainVM>
</Window.DataContext>
<Window.Resources>
<DataTemplate x:Key="View1Template" DataType="{x:Type VM:CustomerVM}">
<View:Customers DataContext="{Binding VM:CustomerVM}" />
</DataTemplate>
<DataTemplate x:Key="View2Template" DataType="{x:Type VM:SuppliersVM}">
<View:Suppliers DataContext="{Binding VM:Suppliers}"/>
</DataTemplate>
</Window.Resources>
<ContentControl Margin="0,135,0,10" Grid.Column="1">
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}">
<Setter Property="ContentTemplate" Value="{StaticResource View1Template}" />
<Style.Triggers>
<DataTrigger Binding="{Binding SwitchView}" Value="0">
<Setter Property="ContentTemplate" Value="{StaticResource View1Template}" />
</DataTrigger>
<DataTrigger Binding="{Binding SwitchView}" Value="1">
<Setter Property="ContentTemplate" Value="{StaticResource View2Template}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
this has its own ViewModel and navigation loads the view correctly, however i have a command which runs on load and its not performing. This just populates a listview. I know it works because if i remove the datacontext of the window and set it as CustomerVM
the listview is populated however navigation no longer works because MainVM
has been removed.
Customer.xaml
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding LoadCustomersCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<Grid>
<TextBox Tag="Search Customers" x:Name="searchTextBox" Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" HorizontalAlignment="Left" Height="40" Margin="10,9,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="340" Padding="5,6,0,0" FontSize="16"/>
<Label Content="{Binding Path=SearchText}" Margin="430,0,436,263" />
<ListView ItemsSource="{Binding Customers}" x:Name="customerListBox" Margin="10,57,10,10" AlternationCount="2" >
<ListView.View>
<GridView>
<GridViewColumn Header="ID" Width="Auto" DisplayMemberBinding="{Binding id}" />
<GridViewColumn Header="NAME" Width="Auto" DisplayMemberBinding="{Binding name}" />
<GridViewColumn Header="ADDRESS" Width="Auto" DisplayMemberBinding="{Binding address1}" />
<GridViewColumn Header="ADDRESS 2" Width="150" DisplayMemberBinding="{Binding address2}" />
<GridViewColumn Header="TOWN" Width="150" DisplayMemberBinding="{Binding town}" />
<GridViewColumn Header="COUNTY" Width="150" DisplayMemberBinding="{Binding county}" />
<GridViewColumn Header="POSTCODE" Width="150" DisplayMemberBinding="{Binding postcode}" />
<GridViewColumn Header="PHONE" Width="150" DisplayMemberBinding="{Binding phone}" />
<GridViewColumn Header="EMAIL" Width="150" DisplayMemberBinding="{Binding email}" />
</GridView>
</ListView.View>
</ListView>
</Grid>
CustomerVM.cs
private string _searchText;
private readonly ObservableCollection<Customer> _customers = new ObservableCollection<Customer>();
public string Title = "Customers";
public string SearchText
{
get { return _searchText; }
set
{
_searchText = value;
RaisePropertyChangedEvent("SearchText");
}
}
public IEnumerable<Customer> Customers
{
get { return _customers; }
}
public ICommand LoadCustomersCommand
{
get { return new DelegateCommand(LoadCustomers); }
}
public void LoadCustomers()
{
Customer cus = new Customer { id = 1, name = "sam" };
_customers.Add(cus);
}
MainVM.cs
public ICommand NavigationClick
{
get { return new DelegateCommand(Navigate); }
}
public void Navigate()
{
SwitchView = 1;
}
Upvotes: 3
Views: 243
Reputation: 37059
I think there are several things going on here. We'll begin with the one that's most clear to me at the outset.
A DataContext
is an object -- generally an instance of a viewmodel class. A DataTemplate
is used to display an instance of a viewmodel class. Its DataContext
is inherited by the controls that it contains. This is probably what you want:
<Window.Resources>
<DataTemplate DataType="{x:Type VM:CustomerVM}">
<View:Customers />
</DataTemplate>
<DataTemplate DataType="{x:Type VM:SuppliersVM}">
<View:Suppliers />
</DataTemplate>
</Window.Resources>
Secondly, you have to populate your ContentControl with an actual instance of one child viewmodel or another.
One way you've found is to give each view a <DataContext>
element in the XAML that creates a viewmodel instance:
<DataContext>
<VM:CustomerVM />
</DataContext>
But then how does the main viewmodel interact with its children? One solution is to use an MVVM framework that adds a byzantine "locator" that halfway solves the problem with a mess of kludges and workarounds. Much effort is spent satisfying the framework's demands.
That design puts the views in charge of everything, but views have no logic. They have no business being in charge. The "brain" of your application is broken into a dozen little pieces that are hidden from each other.
The other way is "viewmodel centric design": Design your application as a set of viewmodels. Child viewmodels are properties of parent viewmodels. MainVM
can easily find its child SupplierVM
because MainVM
created it and owns it. Instead of the viewmodels being isolated appendages dangling from the views, the views are isolated appendages of the viewmodels -- which is absolutely fine, because views pretty much just sit there anyway.
Everything with WPF is easier this way.
You've got three viewmodels here: A parent, MainVM
, and two children, CustomerVM
and SupplierVM
. Within the main view, you want to display one child or the other at a time.
So we'll give MainVM
an instance of each child:
public MainVM()
{
Customer = new CustomerVM();
Supplier = new SupplierVM();
}
public CustomerVM Customer {
get { return _customerVM; }
private set { _customerVM = value; }
}
public SupplierVM Supplier {
get { return _supplierVM; }
private set { _supplierVM = value; }
}
public INotifyPropertyChanged _selectedChild;
public INotifyPropertyChanged SelectedChild {
get { return _selectedChild; }
set {
if (value != _selectedChild) {
_selectedChild = value;
// I don't know how you raise PropertyChanged; if it doesn't look
// like this, let me know.
OnPropertyChanged();
}
}
}
public void Navigate()
{
SelectedChild = Customer;
}
In the XAML, the content control is much simplified. Whatever you put in SelectedChild
will be displayed in the content control. How does it know what template to use?
Simple: We took the x:Key
attribute off the data templates above, but we left the DataType
attributes. That makes them into what's called "implicit data templates": Wherever they are in scope, they will be used automatically by any ContentControl
that needs to display an object of one of those types. Anywhere in this Window
, if you tell XAML "here's a CustomerVM
; show it to the user", XAML will use the first of the above two datatemplates to display it.
<ContentControl
Content="{Binding SelectedChild}"
Margin="0,135,0,10"
Grid.Column="1"
/>
You can get rid of the SwitchView
property. What you did certainly works (and as you've found, doing something that works in XAML isn't always easy), but this way is easier and more conventional.
Upvotes: 4