I'm using the following articles for starter code:
Navigating Between Views in WPF MVVM
Simple Injector WPF Integration
Objective:
Trying to go from View 1 to View 2 in WPF form using a button binding command and Simple Injector to inject dependencies into views. Note: These dependencies are repositories that hold data from outside sources.
Issue:
After injecting dependencies into my MainWindow and MainWindowViewModel using Simple Injector, my buttons no longer change my current view (to another one of my views). When using Visual Studio and debugging using breakpoints, the code seems to be stuck in a loop forever in the CanExecute
function of RelayCommand.cs (see Navigating Between Views in WPF MVVM), where something is calling it over and over again. I can't debug more into the CanExecute
function because there's a lot of code that gets passed over (from DLLs and such). When not using breakpoints, it just appears as if my button does nothing.
I get no button errors in the output window and no exceptions are thrown. The command binding is working because I can see the function OnGo2Screen
found in MainWindowViewModel.cs being called while debugging. After the OnGo2Screen
is called, it moves through the code as expected until it gets stuck in the CanExecute
.
What I've Tried
I have checked my MainWindow's data context and I can see that it has all the correct functions.
I made a separate project for the Navigating Between Views in WPF MVVM article and I was able to get the views to change just fine. But whenever I try to use Simple Injector, my buttons break.
I noticed that when not using Simple Injector, the code moves from the CanExecute
function to the CanExecuteChanged
EventHandler and does the remove and add mutators, and afterwards change the view as expected. However when using Simple Injector, it does not do this.
The Code
I'm using my App.xaml.cs as the startup program where my App.xaml has a Build Action of "Page".
SimulationCaseView is View 1 (The default starting view).
StreamsView is View 2 (Just another view).
UserControl3 is View 3 (Just another view).
Below is my code. Refer to the two links provided for any remaining code, as I based a lot of the functions on that.
App.xaml
<Application x:Class="MyApp.Desktop.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:MyApp.Desktop.Views">
<Application.Resources>
<DataTemplate DataType="{x:Type views:SimulationCaseViewModel}">
<views:SimulationCaseView />
</DataTemplate>
<DataTemplate DataType="{x:Type views:StreamsViewModel}">
<views:StreamsView />
</DataTemplate>
<DataTemplate DataType="{x:Type views:UserControl3ViewModel}">
<views:UserControl3 />
</DataTemplate>
</Application.Resources>
</Application>
App.xaml.cs
namespace MyApp.Desktop
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
App()
{
InitializeComponent();
}
[STAThread]
static void Main()
{
var container = Bootstrap();
// Any additional other configuration, e.g. of your desired MVVM toolkit.
RunApplication(container);
}
private static Container Bootstrap()
{
// Create the container as usual.
var container = new Container();
// Register your types, for instance:
container.Register<IPreferencesRepository, PreferencesRepository>(Lifestyle.Singleton);
container.Register<IStreamRepository, StreamRepository>(Lifestyle.Singleton);
// Register your windows and view models:
container.Register<MainWindow>();
container.Register<MainWindowViewModel>();
container.Verify();
return container;
}
private static void RunApplication(Container container)
{
try
{
var app = new App();
var mainWindow = container.GetInstance<MainWindow>();
MainWindowViewModel viewModel = container.GetInstance<MainWindowViewModel>();
mainWindow.DataContext = viewModel;
app.Run(mainWindow);
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
}
}
MainWindow.xaml
<Window x:Class="MyApp.Desktop.Views.MainWindow"
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:MyApp.Desktop"
mc:Ignorable="d"
Title="MainWindow"
Height="350" Width="525"
xmlns:views="clr-namespace:MyApp.Desktop.Views">
<Grid>
<ContentControl Content="{Binding CurrentPageViewModel}" />
</Grid>
</Window>
MainWindowViewModel.cs
namespace MyApp.Desktop.Views
{
public class MainWindowViewModel : 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 OnGo1Screen(object obj)
{
ChangeViewModel(PageViewModels[0]);
}
private void OnGo2Screen(object obj)
{
ChangeViewModel(PageViewModels[1]);
}
private void OnGo3Screen(object obj)
{
ChangeViewModel(PageViewModels[2]);
}
public MainWindowViewModel(IStreamRepository streamRepository)
{
// Add available pages and set page
PageViewModels.Add(new SimulationCaseViewModel(streamRepository));
PageViewModels.Add(new StreamsViewModel());
PageViewModels.Add(new UserControl3ViewModel());
CurrentPageViewModel = PageViewModels[0];
Mediator.Subscribe("GoTo1Screen", OnGo1Screen);
Mediator.Subscribe("GoTo2Screen", OnGo2Screen);
Mediator.Subscribe("GoTo3Screen", OnGo3Screen);
}
}
}
SimulationCaseView.xaml
<UserControl x:Class="MyApp.Desktop.Views.SimulationCaseView"
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:MyApp.Desktop"
mc:Ignorable="d"
d:DesignHeight="280" d:DesignWidth="280">
<Grid>
<Button
Content="Go to Streams"
Command="{Binding GoTo2}"
Width="90" Height="30" Margin="166,220,24,30">
</Button>
</Grid>
</UserControl>
SimulationCaseViewModel.cs
namespace MyApp.Desktop.Views
{
public class SimulationCaseViewModel : BaseViewModel, IPageViewModel
{
private ICommand _goTo2;
private readonly IStreamRepository _repo;
public SimulationCaseViewModel(IStreamRepository repo)
{
_repo = repo;
Application application = _repo.GetApplicationReference();
CurrentSimulationCases = new ObservableCollection<SimulationCase>();
Streams = new ObservableCollection<object>();
foreach (SimulationCase simulationCase in application.SimulationCases)
{
CurrentSimulationCases.Add(simulationCase);
}
//FetchStreams = new RelayCommand(OnFetch);
}
public ObservableCollection<SimulationCase> CurrentSimulationCases { get; set; }
public ObservableCollection<object> Streams { get; private set; }
public ICommand GoTo2
{
get
{
return _goTo2 ?? (_goTo2 = new RelayCommand(x =>
{
Mediator.Notify("GoTo2Screen", "");
}));
}
}
}
}
Any help as to why the buttons aren't working is appreciated. Thanks.
The problem is in the Lifestyle
of your ViewModel that must be set to a Singleton
and not to the default Transient
.
private static Container Bootstrap()
{
// Create the container as usual.
var container = new Container();
// Register your types, for instance:
// Register your windows and view models:
//container.Register<MainWindow>(Lifestyle.Singleton); //not needed
container.Register<MainWindowViewModel>(Lifestyle.Singleton);
container.Verify();
return container;
}
Then you can start the app in a simple way
private static void RunApplication(Container container)
{
try
{
var mainWindow = container.GetInstance<MainWindow>();
var app = new App();
app.InitializeComponent();
app.Run(mainWindow);
}
catch (Exception ex)
{
//Log the exception and exit
Debug.WriteLine(ex.Message);
}
}
The full code is on github.
When you call the container.Verify
in the Bootstrap
you will create an instance of MainWindowViewModel
to verify its instantiation and another to verify the MainWindow
class.
Incidentally, you could solve your issue by simply not verifying the container!
So the 2nd solution is
//container.Register<MainWindow>(); // => Lifestyle.Transient;
container.Register<MainWindowViewModel>(); // => Lifestyle.Transient;
//container.Verify();
Now, notice that you have the Mediator
subscription in the MainWindowViewModel
c.tor.
public static void Subscribe(string token, Action<object> callback)
{
if (!pl_dict.ContainsKey(token))
{
var list = new List<Action<object>>();
list.Add(callback);
pl_dict.Add(token, list);
}
else
{
bool found = false;
//foreach (var item in pl_dict[token])
// if (item.Method.ToString() == callback.Method.ToString())
// found = true;
if (!found)
pl_dict[token].Add(callback);
}
}
The foreach
loop - that I've commented only above (and it is the 3rd alternative option to solve your issue) - will make you skip the call to the second correct ViewModel's methods and will leave you with the first wrong ones (remember that the Bootstrap
verification created it twice).
If you want a 4th alternative solution, with the classical IComponent
interface of the Mediator pattern
public interface IComponent
{
void OnGo1Screen(object obj);
void OnGo2Screen(object obj);
}
public class MainWindowViewModel : BaseViewModel, IComponent
you can also move the subscription out of the c.tor
public MainWindowViewModel()
{
// Add available pages and set page
PageViewModels.Add(new UserControl1ViewModel());
PageViewModels.Add(new UserControl2ViewModel());
CurrentPageViewModel = PageViewModels[0];
//Mediator.Subscribe("GoTo1Screen", OnGo1Screen);
//Mediator.Subscribe("GoTo2Screen", OnGo2Screen);
}
into your Program
:
var context = mainWindow.DataContext as IComponent;
Mediator.Subscribe("GoTo1Screen", context.OnGo1Screen);
Mediator.Subscribe("GoTo2Screen", context.OnGo2Screen);