Cosmin
Cosmin

Reputation: 431

New instance of MVVM Window breaks the bindings

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.

Here is my project.

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

Answers (2)

BionicCode
BionicCode

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.

Remarks

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.

Solution

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

Zaheer
Zaheer

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

Related Questions