Justin Shep Justin Shep - 4 months ago 61
Vb.net Question

Ensure each child UserControl creates its own ViewModel instance

I am quite new to WPF and the MVVM pattern so please bare with me. I working on a project for a client and decided to utilize the benefits of WPF in terms of data binding and the declarative approach to UI design. But I'm having a huge issue understanding the relationship between my Views and ViewModels.

I have a UserControl (ParentUserControl), and a child UserControl (ChildUserControl). Within this ParentUserControl I have a ContentPresenter that can hold multiple instances of ChildUserControl. The ChildUserControl has multiple comboboxes and textboxes displaying information from my Model. The user can open as many ChildUserControls within the ParentUserControl as they wish by clicking an 'Add New' Button. In my ParentViewModel, I'm storing the instances of each ChildViewModel that is created with the user adds a new ChildUserControl to the ParentUserControl. The user can navigate through the ChildUserControls through 'View Next' and 'View Previous' Buttons.

All of this works great, except if a user makes a selection or changes the text of any control in any ChildUserControl, the change propagates throughout all the ChildUserControls the user has created / sharing one ViewModel for all views. I've tried everything that I can think of, with and without using the MVVM Light tool kit (which seems like it's going to save me a ton of time in the future).

My question is how can I be absolutely positively sure that each time a new View is instantiated that it gets its own instance of it's ViewModel? I've been stuck on this for days!!! Thanks!

'NOTE: This code is not of an actual product. It's to simply demonstrate the issue I'm having with a real application. Don't be afraid to answer in C# if need be, my preferred language anyway. Oh, and I'm trying to complete this project with Pure MVVM in mind. Thank you!!!

Code:

ParentUserControl:

<UserControl x:Class="ParentUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MVVM_Light_Test_Application"
Height="350" Width="525">
<UserControl.Resources>
<DataTemplate DataType="{x:Type local:ChildUserControlViewModel}">
<local:ChildUserControl/>
</DataTemplate>
</UserControl.Resources>

<UserControl.DataContext>
<local:MainViewModel/>
</UserControl.DataContext>
<Grid>
<StackPanel Width="auto" Height="200">
<ContentPresenter Content="{Binding CurrentView}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >
</ContentPresenter>
</StackPanel>
<StackPanel>
<Button Command="{Binding ChangeUserControlCommand}" Content="Click To Change View"/>
</StackPanel>
</Grid>
</UserControl>


ChildUserControl:

<UserControl x:Class="ChildUserControl"
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:MVVM_Light_Test_Application"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300" Background="{Binding BGColor, UpdateSourceTrigger=PropertyChanged}">
<UserControl.DataContext>
<local:ChildUserControlViewModel/>
</UserControl.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="149*"/>
<ColumnDefinition Width="151*"/>
</Grid.ColumnDefinitions>

<StackPanel Height="200" Width="auto">
<StackPanel>
<ComboBox x:Name="cboExampleObjects" Margin="5" ItemsSource="{Binding Customers}" DisplayMemberPath="Details" SelectedItem="{Binding SelectedCustomer}"/>
</StackPanel>
<StackPanel Grid.Column="1">
<TextBox x:Name="txtObjectPropertyValue" Text="{Binding SelectedCustomer.Name}" Margin="5"/>
</StackPanel>
</StackPanel>
</Grid>
</UserControl>


Parent View Model:

Public Class MainViewModel
Inherits ViewModelBase

Public Sub New()
_currentView = New ChildUserControlViewModel
ChangeUserControlCommand = New RelayCommand(AddressOf ChangeUserControl)
End Sub

Private _currentView As ViewModelBase
Public Property CurrentView As ViewModelBase
Get
Return _currentView
End Get
Set(value As ViewModelBase)
_currentView = value
RaisePropertyChanged("CurrentView")
End Set
End Property

Public Property ChangeUserControlCommand As RelayCommand


Public Sub ChangeUserControl()
CurrentView = New ChildUserControlViewModel
CType(CurrentView, ChildUserControlViewModel).BGColor = Nothing
End Sub
End Class


Child View Model:

Imports System.Collections.ObjectModel

Public Class ChildUserControlViewModel
Inherits ViewModelBase

Public Sub New()
_customes = New ObservableCollection(Of Customer)(New List(Of Customer)({New Customer With {.Name = "TestName1", .CustomerNumber = 1}, New Customer With {.Name = "TestName2", .CustomerNumber = 2}}))
End Sub

Private _customers As ObservableCollection(Of Customer)
Public Property Customers As ObservableCollection(Of Customer)
Get
Return _customers
End Get
Set(value As ObservableCollection(Of Customer))
_customers = value
RaisePropertyChanged("Customers")
End Set
End Property

Private _selectedCustomer As Customer
Public Property SelectedCustomer As Customer
Get
Return _selectedCustomer
End Get
Set(value As Customer)
If _selectedCustomer Is Nothing OrElse String.Compare(value.Name, _selectedCustomer.Name) <> 0 Then
_selectedCustomer = value
RaisePropertyChanged("SelectedCustomer")
End If
End Set
End Property

Private _bgColor As SolidColorBrush
Public Property BGColor As SolidColorBrush
Get
Dim random As New Random
Return New SolidColorBrush(Color.FromArgb(50, Random.Next(0, 255), Random.Next(0, 255), Random.Next(0, 255)))
End Get
Set(value As SolidColorBrush)
Dim random As New Random
_bgColor = New SolidColorBrush(Color.FromArgb(50, random.Next(0, 255), random.Next(0, 255), random.Next(0, 255)))
RaisePropertyChanged("BGColor")
End Set
End Property
End Class

Answer

I see several issues:

  1. You set ChildUserControl.DataContext in XAML, but instances of ChildUserControl already get data context from binding in the ContentPresenter. You can safely remove it.

  2. ContentPresenter can contain only one child control. When you create a new view model and set it to CurrentView, the old view model is forgotten, the old ChildUserControl is removed, then a new ChildUserControl is created which represents the new view model.

How do you navigate between child views? I don't see the relevant code.