Reputation: 431
It’s my first week into WPF from Windows Forms and I’m already forced to go to MVVM pattern because almost every tutorial or Stack Overflow answer I stumble upon has this kind of pattern in mind.
Because I have already put a lot of work in my existing project, I am testing MVVM pattern on a single window, in order to see its potential.
Having this cleared, I have Window1, UserControl1 and UserControl2 views, each one having a corresponding ViewModel
.
The Window1 is used to navigate between UserControl1 and UserControl2 and each of the user controls has a button for switching to the other user control.
The navigation is based on the tutorial that Rachel Lim provided (https://rachel53461.wordpress.com/2011/12/18/navigation-with-mvvm-2/. Everything seems to work as intended at the first instance of Window1.
However, if I close Window1 instance and open another one, the buttons in the user controls no longer change the ControlControl that is placed in Window1.
Also, if keep the first Window1 open, create another Window1 instance and click the button that would set UserControl2 as content control in the second Window1 instance, it is not that instance that changes its content control but the first one.
My conclusion is that, somehow, all the bindings are made with the first instance that it is launched but I cannot figure out why.
Some relevant part of XAML and CS codes:
Method for launching Window1, that is located in another Window:
Window1 window = new Window1();
Window1ViewModel context = new Window1ViewModel();
window.DataContext = context;
window.Show();
XAML of Window1:
<Window.Resources>
<DataTemplate DataType="{x:Type local:UserControl1ViewModel}">
<local:UserControl1 />
</DataTemplate>
<DataTemplate DataType="{x:Type local:UserControl2ViewModel}">
<local:UserControl2 />
</DataTemplate>
</Window.Resources>
<Grid>
<ContentControl Content="{Binding CurrentPageViewModel}" />
</Grid>
Window1ViewModel class
class Window1ViewModel : BaseViewModel
{
private IPageViewModel _currentPageViewModel;
private List<IPageViewModel> _pageViewModels;
public List<IPageViewModel> PageViewModels
{
get
{
if (_pageViewModels == null)
_pageViewModels = new List<IPageViewModel>();
return _pageViewModels;
}
}
public IPageViewModel CurrentPageViewModel
{
get
{
return _currentPageViewModel;
}
set
{
_currentPageViewModel = value;
OnPropertyChanged("CurrentPageViewModel");
}
}
private void ChangeViewModel(IPageViewModel viewModel)
{
if (!PageViewModels.Contains(viewModel))
PageViewModels.Add(viewModel);
CurrentPageViewModel = PageViewModels
.FirstOrDefault(vm => vm == viewModel);
}
private void OnGoTo1(object obj)
{
ChangeViewModel(PageViewModels[0]);
}
private void OnGoTo2(object obj)
{
ChangeViewModel(PageViewModels[1]);
}
public Window1ViewModel()
{
// Add available pages and set page
PageViewModels.Clear();
PageViewModels.Add(new UserControl1ViewModel());
PageViewModels.Add(new UserControl2ViewModel());
CurrentPageViewModel = PageViewModels[0];
Mediator.Subscribe("GoTo1", OnGoTo1);
Mediator.Subscribe("GoTo2", OnGoTo2);
}
}
What am I missing here? I cannot figure why it keeps all the bindings with the first Window instance, even if I am creating a new class.
Upvotes: 0
Views: 307
Reputation: 29018
I guess the navigation buttons are not part of the Window1
control and bind to the Window1ViewModel
, which is the DataContext
of Window1
. If so, you have to reuse the same Window1ViewModel
instance for every instance of Window1
.
MainWindow.xaml.cs
partial class MainWindow : Window
{
private Window1ViewModel Window1ViewModel { get; set; }
public MainWindow()
{
this.Window1ViewModel = new Window1ViewModel();
}
private void ShowWindow1()
{
Window1 window = new Window1();
window.DataContext = this.Window1ViewModel;
window.Show();
}
}
If creating a new instance of Window1ViewModel
is a requirement, then you should redesign the view and move the navigation buttons to the Window1
control.
I'll try to explain why you have to reuse the initial instance of Window1ViewModel
in your current implementation.
It's a matter of scope and instances or instance references.
Let's take your initial setup as context: we have a first control e.g., a Button
that binds to the DataContext
e.g. Window1ViewModel
of a second control e.g., Window1
.
The bindings to Window1ViewModel
initially work, but when you close Window1
and open a new instance of Window1
, then those bindings no longer work.
To understand what's really happening, you have to remember, that you are not dealing with classes, but with instances of classes. You can have multiple instances of the same class.
Generally, the binding information (source object and target object of the data binding) is stored in an instance of the class Binding
.
Now, when setting up a XAML binding on a property of the first control (the Button.Command
) e.g. to bind to a command of a Window1ViewModel
instance, the framework will create a new instance of Binding
, where its Binding.Target
property is set to Button.Command
(a property on the current instance of Button
) and the Binding.Source
property is set to the current (first) instance of Window1ViewModel
(and to the instance's property e.g. NextPageCommand
).
You show the window like this:
private void ShowWindow1()
{
Window1 window = new Window1();
window.DataContext = new Window1ViewModel();
window.Show();
}
When you now close Window1
and leave the scope of the window
instance variable, you cannot access the first Window1ViewModel
instance anymore, because the only reference to this instance was stored in the DataContext
property of Window1
. But still, the binding of the Button
references the first instance of Window1ViewModel
.
You then decide to show a new window and instantiate a new (second) instance of Window1
and assign it a new (second) instance of Window1ViewModel
. How does the binding now about the new instance of Window1ViewModel
?
Even when you are reusing the first Window1
instance and just add a new instance of Window1ViewModel
to the Window1.DataContext
, the binding still references the very first (initial) instance of Window1ViewModel
.
Binding
doesn't derive from DependencyObject
and therefore doesn't implement its properties as DependencyProperty
. This means Binding.Source
is not a DependencyProperty
and can't trigger property changes and therefore will not update the reference to point to the second instance of Window1ViewModel
. That's why reusing the initial instance of Window1ViewModel
solves the problem (Binding.Source
still references it).
Alternatively you could've replaced the Binding
instance when replacing the binging source instance. But this would require to write more complex C# code, without XAML designer help to resolve the current DataContext
.
When taking a look at the logic you are trying to implement, it really doesn't make sense to have a single set of navigation buttons to navigate multiple independent windows.
If you decide to have multiple Window1
instances running in parallel, then you must let Window1
handle the navigation by itself.
Window1.xaml
<window>
<StackPanel>
<Button x:Name="LoadPreviousButton"
Command="ShowPreviousCommand}" />
<Button x:Name="LoadNextButton"
Command="ShowNextCommand}" />
<ContentPresenter Content="{Binding CurrentPage}" />
</StackPanel>
</Window>
Now you can have as much Window1
instances as you like, where each Window1
instance can have a dedicated instance of Window1ViewModel
:
// This will now behave as you expected it to
var window = new Window1() { DataContext = new Window1ViewModel() };
window.Show();
Upvotes: 2
Reputation: 375
You have to make two changes. One is, you have to Unsubscribe Mediator in Window1ViewModel.cs
public Window1ViewModel()
{
Mediator.Unsubscribe("GoTo1", OnGoTo1);
Mediator.Unsubscribe("GoTo2", OnGoTo2);
// Add available pages and set page
PageViewModels.Clear();
PageViewModels.Add(new UserControl1ViewModel());
PageViewModels.Add(new UserControl2ViewModel());
CurrentPageViewModel = PageViewModels[0];
Mediator.Subscribe("GoTo1", OnGoTo1);
Mediator.Subscribe("GoTo2", OnGoTo2);
}
Second, you have to update Unsubscribe method in Mediator class:
public static void Unsubscribe(string token, Action<object> callback)
{
if (pl_dict.ContainsKey(token)) {
//pl_dict[token].Remove(callback);
pl_dict.Remove(token);
}
}
Before, this method was removing callback of token only, but it should actually remove the Subscription entry.
Upvotes: 0