I'm using NInject to resolve the dependency for my first WPF application.
Following are my code snippets.
My App.xaml.cs goes like.
public partial class App : Application
{
private IKernel container;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
ConfigureContainer();
ComposeObjects();
}
private void ComposeObjects()
{
Current.MainWindow = this.container.Get<MainWindow>();
}
private void ConfigureContainer()
{
this.container = new StandardKernel();
container.Bind<ISystemEvents>().To<MySystemEvents>();
}
}
App.xaml goes like this.
<Application x:Class="Tracker.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Application.Resources>
</Application.Resources>
</Application>
MainWindow.xaml.
<Window x:Class="Tracker.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewmodel="clr-namespace:Tracker.ViewModel"
Title="MainWindow" Height="150" Width="350">
<Window.DataContext>
<viewmodel:TrackerViewModel>
</viewmodel:TrackerViewModel>
</Window.DataContext>
<Grid>
</Grid>
</Window>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
and viewmodel
internal class TrackerViewModel : System.ComponentModel.INotifyPropertyChanged
{
public TrackerViewModel(ISystemEvents systemEvents)
{
systemEvents.SessionSwitch += SystemEvents_SessionSwitch;
}
private void SystemEvents_SessionSwitch(object sender, SessionSwitchEventArgs e)
{
}
}
Now when I launch the application, I get an exception An unhandled exception of type 'System.NullReferenceException' occurred in PresentationFramework.dll in InitializeComponent() method.
I know its because of the viewmodel class not have parameterless constructor. But I am not able to undestand why dependency injector is not able to resolve this? Am I doing something wrong?
Any help would be greatly appreciated.
First of all, I recommend reading the book Dependency Injection in .NET, especially the section about WPF. But even if you don't read it, there is a helpful example in the code download for the book.
You have already worked out that you need to remove the StartupUri="MainWindow.xaml" from your App.xaml file.
However, when using DI you must not wire up the DataContext declaratively otherwise it will only be able to work with the default constructor.
<Window x:Class="WpfWithNinject.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="150" Width="350">
</Window>
The pattern that is used in WPF is a bit confusing when it comes to DI. The main issue is that if you want your ViewModel to be able to control its own windowing environment, there is a circular dependency issue between the MainWindow and its ViewModel, so you will need to make an Abstract Factory in order to instantiate the ViewModel so the dependencies can be satisfied.
Creating the ViewModel Factory
internal interface ITrackerViewModelFactory
{
TrackerViewModel Create(IWindow window);
}
internal class TrackerViewModelFactory : ITrackerViewModelFactory
{
private readonly ISystemEvents systemEvents;
public TrackerViewModelFactory(ISystemEvents systemEvents)
{
if (systemEvents == null)
{
throw new ArgumentNullException("systemEvents");
}
this.systemEvents = systemEvents;
}
public TrackerViewModel Create(IWindow window)
{
if (window == null)
{
throw new ArgumentNullException("window");
}
return new TrackerViewModel(this.systemEvents, window);
}
}
The TrackerViewModel also needs to have some rework so it can accept the IWindow into its constructor. This allows the TrackerViewModel to control its own windowing environment, such as showing modal dialog boxes to the user.
internal class TrackerViewModel : System.ComponentModel.INotifyPropertyChanged
{
private readonly IWindow window;
public TrackerViewModel(ISystemEvents systemEvents, IWindow window)
{
if (systemEvents == null)
{
throw new ArgumentNullException("systemEvents");
}
if (window == null)
{
throw new ArgumentNullException("window");
}
systemEvents.SessionSwitch += SystemEvents_SessionSwitch;
this.window = window;
}
private void SystemEvents_SessionSwitch(object sender, SessionSwitchEventArgs e)
{
}
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
}
Adapting the Window
You need to fix up the framework a bit with an abstract type for the windows, IWindow, and an abstraction to help manage DI of each of the windows, WindowAdapter.
internal interface IWindow
{
void Close();
IWindow CreateChild(object viewModel);
void Show();
bool? ShowDialog();
}
internal class WindowAdapter : IWindow
{
private readonly Window wpfWindow;
public WindowAdapter(Window wpfWindow)
{
if (wpfWindow == null)
{
throw new ArgumentNullException("window");
}
this.wpfWindow = wpfWindow;
}
#region IWindow Members
public virtual void Close()
{
this.wpfWindow.Close();
}
public virtual IWindow CreateChild(object viewModel)
{
var cw = new ContentWindow();
cw.Owner = this.wpfWindow;
cw.DataContext = viewModel;
WindowAdapter.ConfigureBehavior(cw);
return new WindowAdapter(cw);
}
public virtual void Show()
{
this.wpfWindow.Show();
}
public virtual bool? ShowDialog()
{
return this.wpfWindow.ShowDialog();
}
#endregion
protected Window WpfWindow
{
get { return this.wpfWindow; }
}
private static void ConfigureBehavior(ContentWindow cw)
{
cw.WindowStartupLocation = WindowStartupLocation.CenterOwner;
cw.CommandBindings.Add(new CommandBinding(PresentationCommands.Accept, (sender, e) => cw.DialogResult = true));
}
}
public static class PresentationCommands
{
private readonly static RoutedCommand accept = new RoutedCommand("Accept", typeof(PresentationCommands));
public static RoutedCommand Accept
{
get { return PresentationCommands.accept; }
}
}
Then we have a specialized window adapter for the MainWindow which ensures the DataContext property is initialized correctly with the ViewModel.
internal class MainWindowAdapter : WindowAdapter
{
private readonly ITrackerViewModelFactory vmFactory;
private bool initialized;
public MainWindowAdapter(Window wpfWindow, ITrackerViewModelFactory viewModelFactory)
: base(wpfWindow)
{
if (viewModelFactory == null)
{
throw new ArgumentNullException("viewModelFactory");
}
this.vmFactory = viewModelFactory;
}
#region IWindow Members
public override void Close()
{
this.EnsureInitialized();
base.Close();
}
public override IWindow CreateChild(object viewModel)
{
this.EnsureInitialized();
return base.CreateChild(viewModel);
}
public override void Show()
{
this.EnsureInitialized();
base.Show();
}
public override bool? ShowDialog()
{
this.EnsureInitialized();
return base.ShowDialog();
}
#endregion
private void DeclareKeyBindings(TrackerViewModel vm)
{
//this.WpfWindow.InputBindings.Add(new KeyBinding(vm.RefreshCommand, new KeyGesture(Key.F5)));
//this.WpfWindow.InputBindings.Add(new KeyBinding(vm.InsertProductCommand, new KeyGesture(Key.Insert)));
//this.WpfWindow.InputBindings.Add(new KeyBinding(vm.EditProductCommand, new KeyGesture(Key.Enter)));
//this.WpfWindow.InputBindings.Add(new KeyBinding(vm.DeleteProductCommand, new KeyGesture(Key.Delete)));
}
private void EnsureInitialized()
{
if (this.initialized)
{
return;
}
var vm = this.vmFactory.Create(this);
this.WpfWindow.DataContext = vm;
this.DeclareKeyBindings(vm);
this.initialized = true;
}
}
The Composition Root
And finally, you need a way to create the object graph. You are doing that in the correct place, but you are not doing yourself any favors by breaking it into many steps. And putting the container as an application-level variable is not necessarily a good thing - it opens up the container for abuse as a service locator.
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// Begin Composition Root
var container = new StandardKernel();
// Register types
container.Bind<ISystemEvents>().To<MySystemEvents>();
container.Bind<ITrackerViewModelFactory>().To<TrackerViewModelFactory>();
container.Bind<Window>().To<MainWindow>();
container.Bind<IWindow>().To<MainWindowAdapter>();
// Build the application object graph
var window = container.Get<IWindow>();
// Show the main window.
window.Show();
// End Composition Root
}
}
I think the main issue you are having is that you need to ensure to call Show() on the MainWindow manually.
If you really do want to break the registration out into another step, you can do so by using one or more Ninject Modules.
using Ninject.Modules;
using System.Windows;
public class MyApplicationModule : NinjectModule
{
public override void Load()
{
Bind<ISystemEvents>().To<MySystemEvents>();
Bind<ITrackerViewModelFactory>().To<TrackerViewModelFactory>();
Bind<Window>().To<MainWindow>();
Bind<IWindow>().To<MainWindowAdapter>();
}
}
And then the App.xaml.cs file will look like this:
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// Begin Composition Root
new StandardKernel(new MyApplicationModule()).Get<IWindow>().Show();
// End Composition Root
}
}
The trackerviewmodel will be instantiated by the auto-generated xaml designer code, not by ninject.
I've never used ninject, but I think you need to configure the container to know about your viewModel, and then inject the viewmodel for Ninject to resolve it and it's dependencies:
public class MainWindow : Window
{
[Inject]
public TrackerViewModel ViewModel { get; set; }
public MainWindow()
{
InitializeComponent();
DataContext = ViewModel;
}
}
Related
I am new to WPF/Prism and I am trying to do the following: I need to load a Login View ( windows or user control) before launching my Main Window. And after successful login, remove the Login view and go to the Main Mindow.
I have looked at several answers within here but all are referencing older versions of PRISM with the boostrapper class.
I have a WPF (Prism 7) application which contains the main project and an Authorization Module.
From my Main project App.xaml.cs
protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
moduleCatalog.AddModule<AuthorizationModule>();
}
In the Authorization module i have my LoginView / LoginViewModel. The authortization Module registers a LoginService which will be injected to the LoginViewModel
public void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.Register(typeof(ILoginService), typeof(LoginService));
}
The LoginViewModel will take care of authenticating the user using this LoginService.
Part of the answers I've seen show something like this:
protected override void InitializeShell()
{
Window login = new LoginView();
var loginVM = new LoginViewModel(new LoginAuth());
loginVM.LoginCompleted += (sender, args) =>
{
login.Close();
Application.Current.MainWindow.Show();
};
login.DataContext = loginVM;
// problem below:
login.ShowDialog();
}
However, this seems a bit off having to instantiate the LoginView manually instead of just having a container do it for you.
Also, the InitiallizeShell on PRISM 7 is expecting the current shell being created. Im not sure if I should use this value being passed to Activate the Main Window.
protected override void InitializeShell(Window shell)
I also read from Brian Lagunas himself on Github to maybe use the EventAggregator (which I've tried). i've had the Authorization Module register the EventAggregator and from the LoginViewModel, on successful login, publish a SuccessfulLoginEvent but I can figure out how to subscribe to that event from Main app
So basically the expected result is that when the application launches, if the user is not logged in, show the LoginView, after the user authenticates him self, take him to the MainWindow with all needed modules already loaded.
Any help would be greatly appreciated.
I gave this a try and came up with the following:
App:
public partial class App : PrismApplication
{
protected override Window CreateShell()
{
return Container.Resolve<Login>();
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.Register(typeof(object), typeof(Login), "Login");
containerRegistry.RegisterInstance(typeof(LoginViewModel), new LoginViewModel(Container.GetContainer(), Container.Resolve<RegionManager>()));
containerRegistry.Register(typeof(object), typeof(MainWindow), "MainWindow");
containerRegistry.RegisterInstance(typeof(MainWindowViewModel), new MainWindowViewModel(Container.GetContainer(), Container.Resolve<RegionManager>()));
}
}
XAML:
<Window x:Class="LoginTest.Views.Login"
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:LoginTest.Views"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d"
Title="Login" Height="250" Width="400">
<Grid>
<Button Content="Login" HorizontalAlignment="Left" Height="61" Margin="100,100,0,0" VerticalAlignment="Top" Width="164" Command="{Binding LoginCommand}"/>
</Grid>
Codebehind:
public partial class Login : Window
{
public Login()
{
InitializeComponent();
((LoginViewModel)DataContext).NewWindow += StartMainApp;
((LoginViewModel)DataContext).CloseWindow += CloseWindow;
}
private void StartMainApp(Object win)
{
Application.Current.MainWindow = (Window)win;
Application.Current.MainWindow.Show();
}
private void CloseWindow()
{
this.Close();
}
}
ViewModel:
class LoginViewModel : BindableBase
{
private readonly IUnityContainer _container;
private readonly IRegionManager _regionManager;
private PrismApplication _application;
private string _title = "Prism Application";
public string Title
{
get { return _title; }
set { SetProperty(ref _title, value); }
}
public DelegateCommand LoginCommand { get; set; }
public delegate void NewWindowDelegate(Object win);
public delegate void CloseWindowDelegate();
public CloseWindowDelegate CloseWindow{ get; set; }
public NewWindowDelegate NewWindow { get; set; }
public LoginViewModel(IUnityContainer container, IRegionManager regionManager)
{
_regionManager = regionManager;
_container = container;
LoginCommand = new DelegateCommand(OnLogin);
}
private void OnLogin()
{
Trace.WriteLine("Logging in");
// do your login stuff
// If Login OK continue here
NewWindow.Invoke(_container.Resolve<MainWindow>());
CloseWindow.Invoke();
}
}
I hope my example is of any use!
I want to express my gratitude to user947737 for good advice. I tried it myself and it worked out for me.
Additionally, of course, I added the MainWindowsModelView constructor.
public class MainWindowViewModel : BindableBase
{
private readonly IUnityContainer _container;
private readonly IRegionManager _regionManager;
private PrismApplication _application;
//private string _title = "Prism Application";
//public string Title
//{
// get { return _title; }
// set { SetProperty(ref _title, value); }
//}
public MainWindowViewModel(IUnityContainer container, IRegionManager regionManager)
{
_container = container;
_regionManager = regionManager;
}
}
I've been trying to get view-based navigation to work using Prism and regions. I tried going through the documentation on MSDN, but for some reason I can't get it to work, and I don't know what I am doing wrong. So this is what I got so far:
MainShellViewModel.cs
//Private Variables
private readonly IRegionManager _regionManager;
//Public Variables
public DelegateCommand<string> NavigateCommand { get; set; }
//Functions and Methods
public MainShellViewModel(IRegionManager regionManager)
{
//Region Manager
_regionManager = regionManager;
NavigateCommand = new DelegateCommand<string>(Navigate);
Initialize();
}
public void Initialize()
{
//Startup View
_regionManager.RegisterViewWithRegion("ViewMainFrame", typeof(Views.Dashboard));
}
public void Navigate(string uri)
{
//Navigation
if(uri != null)
{
_regionManager.RequestNavigate("ViewMainFrame", uri);
}
}
Side note: One of the many tutorials I followed had me put in that Navigate method, do I need it? I am using the MainShellViewModel as the Main view that is injected on startup.
DashboardViewModel.cs: (Contains the error)
{
//Private Variables
private bool _canExercise = true;
//Public Variables
public bool CanExercise()
{
return _canExercise;
}
RelayCommand _exerciseSelCommand;
public ICommand ExerciseSelCommand
{
get
{
if (_exerciseSelCommand == null)
_exerciseSelCommand = new RelayCommand(ExerciseSel, CanExercise);
return _exerciseSelCommand;
}
}
//Dashboard Functions and Methods
IRegion _regionManager;
private void ExerciseSel()
{
SoundPlayers.ButtonSound();
_regionManager.RequestNavigate(new Uri("ExerciseView", UriKind.Relative)); //This gives me the error, it says it can't be nullable?
}
Here is where I register my containers/views, etc.
Bootstrapper.cs:
protected override void ConfigureContainer()
{
base.ConfigureContainer();
#region Region Register Zone
//register views here!
Container.RegisterType(typeof(object), typeof(Views.LoginView), "LoginView");
Container.RegisterType(typeof(object), typeof(Views.Dashboard), "Dashboard");
Container.RegisterType(typeof(object), typeof(Views.ExerciseView), "SettingsView");
Container.RegisterType(typeof(object), typeof(Views.ResultsView), "ResultsView");
Container.RegisterType(typeof(object), typeof(Views.UserCreationView), "UserCreationView");
#endregion
}
So basically I just want to be able to get from the Dashboard (Which is my current startup view) to any other view that's registered in my containers on a click of a button.
MainShell.xaml:
<Window x:Name="Shell"
x:Class="Equinox.Views.MainShell"
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"
mc:Ignorable="d"
xmlns:prism="http://www.codeplex.com/prism"
prism:ViewModelLocator.AutoWireViewModel="True"
Title="Equinox"
FontFamily="Quicksand"
Height="900"
Width="1500"
SizeToContent="WidthAndHeight"
ResizeMode="CanResize"
Background="#EEF3F4"
WindowStyle="SingleBorderWindow"
Icon="/Equinox;component/favicon.ico"
WindowStartupLocation="CenterScreen">
<!-- Main View Region -->
<ContentControl x:Name="ContentControlMain"
prism:RegionManager.RegionName="ViewMainFrame"
Focusable="False"/>
However, I keep getting errors when I try to make my region take another view. The way I was doing it was by using my DashboardViewModel, and creating another IRegionManager named _regionManager, and doing the RequestNavigation. I got no errors until I ran it and pressed the button that should link me to the next view.
Any help would be appreciated!
Thanks!
Not sure if you got the answer already, but I was running into the same thing today. The comment from Brian gave me the necessary hint.
I have the following code in the bootstrapper. This registers the two views to allow navigation to them:
public class Bootstrapper : UnityBootstrapper
{
protected override DependencyObject CreateShell()
{
return Container.Resolve<MainWindow>();
}
protected override void InitializeShell()
{
Application.Current.MainWindow.Show();
}
protected override void ConfigureContainer()
{
base.ConfigureContainer();
Container.RegisterTypeForNavigation<ViewA>("ViewA");
Container.RegisterTypeForNavigation<ViewB>("ViewB");
}
}
This gives a MainWindow, which registers ViewA and ViewB.
To allow for navigation to ViewB from a button on ViewA, the following needs to be done in ViewAViewModel:
public class ViewAViewModel: BindableBase
{
private readonly IRegionManager _regionManager;
public ViewAViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
ButtonCommand= new DelegateCommand(ButtonClicked);
}
private void ButtonClicked()
{
_regionManager.RequestNavigate("ContentRegion", "ViewB");
}
}
In the XAML form ViewA the last thing you need is of course the button itself:
<Button Content="Navigate" Command="{Binding ButtonCommand}"/>
Check out sampels 17, 18, and 19. This should help get you going in the right direction:
https://github.com/PrismLibrary/Prism-Samples-Wpf
my solutions contains a library called Manager (with one class called ProductionManager) as well as a WPF project in it.
I would like one method of the ProductionManager class to access a UIcontrol of the WPF, how can I do it?
To be more specific my UI there is a button and a TextBox. Pressing the button, I call a method from my library from which I would like to update the TextBox in the UI Window.
I know I should use the Dispatcher to do so but I cannot figure out how to set it up correctly. Can anyone help please?
Here the MainWindow.xaml:
<Window x:Class="WPFMainPanel.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Button Name="btnRun" Content="Run" HorizontalAlignment="Left" Margin="157,10,0,0" VerticalAlignment="Top" Width="75" Click="btnRun_Click"/>
<TextBox Name="tbox_Data" HorizontalAlignment="Left" Height="23" Margin="10,10,0,0" TextWrapping="Wrap" Text="TextBox" VerticalAlignment="Top" Width="120"/>
</Grid>
</Window>
Here the MainWindow.xaml.cs:
namespace WPFMainPanel
{
public partial class MainWindow : Window
{
private ProductionManager myManager = ProductionManager.Instance;
private void btnRun_Click(object sender, RoutedEventArgs e)
{
myManager.DoSomethingElse();
}
}
}
and here my ProductionManager class in my Manager Library
namespace Manager
{
public class ProductionManager
{
private static ProductionManager instance;
private ProductionManager() { }
public static ProductionManager Instance
{
get
{
if (instance == null)
{
instance = new ProductionManager();
}
return instance;
}
}
public void DoSomethingElse()
{
// Change the Text Box Named tbox_Data from here
}
}
}
Can anyone help me please?
You can try something like this.
in your Dosomethingelse() method get a reference to your text box.
public void Dosomethingelse(TextBox textBox,string yourText)
{
var myTextbox = textbox;
mytextbox.text = yourText;
}
now when you are calling the method from MainWindows.xaml.cs call like this,,
myManager.Dosomethingelse(tbox_data,stringYouWantToAdd);
Ok solved!, I found this method from one of iJays' answers.
First you must create a static variable of your MainWindow. So your library can access it. And you must create a string property to set the text of your textbox.Then in the set accessor of that property you can use dispatcher to change the text
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public ProductionManager myManager;
public MainWindow()
{
InitializeComponent();
myManager = new ProductionManager();
window = this;
}
internal static MainWindow window;
public string myString
{
get { return myTextBox.Text; }
//this is where you change the text of your text box
set { Dispatcher.Invoke(new Action(() => { myTextBox.Text = value; })); }
}
private void Button_Click(object sender, RoutedEventArgs e)
{
myManager.DoSomething("Hello world");
}
}
now edit your 'DoSomething()' method as the following,
public void Dosomethingelse(string text)
{
MainWindows.window.myString = text;
}
You can use an event system and the SynchronizationContext class, this way you don't need the extra libraries.
namespace Manager
{
public class ProductionManager
{
private static ProductionManager instance;
// Store the UI context
private readonly SynchronizationContext _UIContext;
// Define event handler signature
public delegate void TextChangedHandler(object sender, TextEventArgs e);
// Define the event to listen for in your MainWindow
public event TextChangedHandler TextChangeEvent;
private ProductionManager()
{
// Set the SynchronizationContext, make sure to call your Instance from the MainWindow
_UIContext = SynchronizationContext.Current;
}
public static ProductionManager Instance
{
get
{
if (instance == null)
{
instance = new ProductionManager();
}
return instance;
}
}
public void DoSomethingElse()
{
// Don't change the TextBox here send the event to the MainWindow instead
// Use the SynchronizationContext to invoke the event
_UIContext.Send(o => TextChangeEvent?.Invoke(this, new TextEventArgs("Hello World!"), null);
}
}
// The TextEventArgs, change for your needs.
public class TextEventArgs : EventArgs
{
public TextEventArgs(string text) : base()
{
Text = text;
}
public string Text { get; }
}
}
Change your MainWindow
public partial class MainWindow : Window
{
private ProductionManager myManager = ProductionManager.Instance;
public MainWindow()
{
InitializeComponent();
// Hookup the event listener
myManager.TextChangeEvent += UpdateTextBox;
}
private void UpdateTextBox(object sender, TextEventArgs e)
{
// Finally handle your text change here
tbox_Data.Text = e.Text;
}
private void btnRun_Click(object sender, RoutedEventArgs e)
{
myManager.DoSomethingElse();
}
}
I'm having a serious headache with this problem. I really dislike store apps but am forced to use it in this case. I've only worked with XAML for a few weeks.
My question is:
How can I call a RelayCommand in my ViewModel (from my View of course) that will change the page on my view? And even better, change it using URI, so that I can pass a command parameter to file.
I'm totally lost on this. Currently I'm using this.Frame.Navigate(type type) in the View Code behind to navigate through pages.
I would really and I mean REALLY appreciate a description from a to z on what to do in this case.
I presume i could do something like building a framecontainer on my View and send it to my ViewModel and from there navigate the current frame to another. But I'm not sure how that works in Store apps.
I am really sorry for the lack of good questions, but I'm on a deadline and i need to get my View connected to my ViewModel in a proper way.. I don't like having both view codebehind as well as ViewModel code.
There are 2 ways to do this, a simple way is to pass a relay command action from the view to the view model.
public MainPage()
{
var vm = new MyViewModel();
vm.GotoPage2Command = new RelayCommand(()=>{ Frame.Navigate(typeof(Page2)) });
this.DataContext = vm;
}
<Button Command={Binding GoToPage2Command}>Go to Page 2</Button>
Another way is by using an IocContainer and DependencyInjection. This one is a more losely coupled approach.
We will need an interface for navigation page so that we don't need to reference or Know anything about PageX or any UI element assuming that your viewmodel is in a separate project that doesn't know anything about the UI.
ViewModel Project:
public interface INavigationPage
{
Type PageType { get; set; }
}
public interface INavigationService
{
void Navigate(INavigationPage page) { get; set; }
}
public class MyViewModel : ViewModelBase
{
public MyViewModel(INavigationService navigationService, INavigationPage page)
{
GotoPage2Command = new RelayCommand(() => { navigationService.Navigate(page.PageType); })
}
private ICommand GotoPage2Command { get; private set; }
}
UI Project:
public class NavigationService : INavigationService
{
//Assuming that you only navigate in the root frame
Frame navigationFrame = Window.Current.Content as Frame;
public void Navigate(INavigationPage page)
{
navigationFrame.Navigate(page.PageType);
}
}
public abstract class NavigationPage<T> : INavigationPage
{
public NavigationPage()
{
this.PageType = typeof(T);
}
}
public class NavigationPage1 : NavigationPage<Page1> { }
public class MainPage : Page
{
public MainPage()
{
//I'll just place the container logic here, but you can place it in a bootstrapper or in app.xaml.cs if you want.
var container = new UnityContainer();
container.RegisterType<INavigationPage, NavigationPage1>();
container.RegisterType<INavigationService, NavigationService>();
container.RegisterType<MyViewModel>();
this.DataContext = container.Resolve<MyViewModel>();
}
}
As Scott says you could use a NavigationService.
I would firstly create an interface this is not needed in this example but will be useful if you use Dependency Injection (good solution with viewmodels and services) in the future :)
INavigationService:
public interface INavigationService
{
void Navigate(Type sourcePage);
void Navigate(Type sourcePage, object parameter);
void GoBack();
}
NavigationService.cs will inherit INavigationService
you will need the following namespaces
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
public sealed class NavigationService : INavigationService
{
public void Navigate(Type sourcePage)
{
var frame = (Frame)Window.Current.Content;
frame.Navigate(sourcePage);
}
public void Navigate(Type sourcePage, object parameter)
{
var frame = (Frame)Window.Current.Content;
frame.Navigate(sourcePage, parameter);
}
public void GoBack()
{
var frame = (Frame)Window.Current.Content;
frame.GoBack();
}
}
Simple ViewModel to show RelayCommand example. NB I Navigate to another Page (Page2.xaml) using the DoSomething RelayCommand.
MyViewModel.cs
public class MyViewModel : INotifyPropertyChanged
{
private INavigationService _navigationService;
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public MyViewModel(INavigationService navigationService)
{
_navigationService = navigationService;
}
private ICommand _doSomething;
public ICommand DoSomething
{
get
{
return _doSomething ??
new RelayCommand(() =>
{
_navigationService.Navigate(typeof(Page2));
});
}
}}
In simple example Ive created the viewmodel in MainPage.cs and added the NavigationService
but you can do this elsewhere depending on what your MVVM setup is like.
MainPage.cs
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
var vm = new MyViewModel(new NavigationService());
this.DataContext = vm;
}
}
MainPage.xaml (binds to the command DoSomething)
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Button Width="200" Height="50" Content="Go to Page 2"
Command="{Binding DoSomething}"/>
</Grid>
Hope that helps.
I don't really like when a ViewModel references Views to navigate to. So I prefer to a ViewModel-first approach. By using ContentControls, DataTemplates for ViewModel types & some kind of navigation pattern in my ViewModels.
My navigation looks like this:
[ImplementPropertyChanged]
public class MainNavigatableViewModel : NavigatableViewModel
{
public ICommand LoadProfileCommand { get; private set; }
public ICommand OpenPostCommand { get; private set; }
public MainNavigatableViewModel ()
{
LoadProfileCommand = new RelayCommand(() => Navigator.Navigate(new ProfileNavigatableViewModel()));
OpenPostCommand = new RelayCommand(() => Navigator.Navigate(new PostEditViewModel { Post = SelectedPost }), () => SelectedPost != null);
}
}
My NavigatableViewModel looks like:
[ImplementPropertyChanged]
public class NavigatableViewModel
{
public NavigatorViewModel Navigator { get; set; }
public NavigatableViewModel PreviousViewModel { get; set; }
public NavigatableViewModel NextViewModel { get; set; }
}
And my Navigator:
[ImplementPropertyChanged]
public class NavigatorViewModel
{
public NavigatableViewModel CurrentViewModel { get; set; }
public ICommand BackCommand { get; private set; }
public ICommand ForwardCommand { get; private set; }
public NavigatorViewModel()
{
BackCommand = new RelayCommand(() =>
{
// Set current control to previous control
CurrentViewModel = CurrentViewModel.PreviousViewModel;
}, () => CurrentViewModel != null && CurrentViewModel.PreviousViewModel != null);
ForwardCommand = new RelayCommand(() =>
{
// Set current control to next control
CurrentViewModel = CurrentViewModel.NextViewModel;
}, () => CurrentViewModel != null && CurrentViewModel.NextViewModel != null);
}
public void Navigate(NavigatableViewModel newViewModel)
{
if (newViewModel.Navigator != null && newViewModel.Navigator != this)
throw new Exception("Viewmodel can't be added to two different navigators");
newViewModel.Navigator = this;
if (CurrentViewModel != null)
{
CurrentViewModel.NextViewModel = newViewModel;
}
newViewModel.PreviousViewModel = CurrentViewModel;
CurrentViewModel = newViewModel;
}
}
My MainWindows.xaml:
<Window
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:viewmodels="clr-namespace:MyApp.ViewModels"
x:Class="MyApp.Windows.MainWindow"
mc:Ignorable="d"
Title="MainWindow" Height="389" Width="573"
d:DataContext="{d:DesignInstance {x:Type viewmodels:MyAppViewModel}, IsDesignTimeCreatable=True}">
<Grid>
<!-- Show data according to data templates as defined in App.xaml -->
<ContentControl Content="{Binding Navigator.CurrentViewModel}" Margin="0,32,0,0" />
<Button Content="Previous" Command="{Binding Navigator.BackCommand}" Style="{DynamicResource ButtonStyle}" HorizontalAlignment="Left" Margin="10,5,0,0" VerticalAlignment="Top" Width="75" />
<Button Content="Next" Command="{Binding Navigator.ForwardCommand}" Style="{DynamicResource ButtonStyle}" HorizontalAlignment="Left" Margin="90,5,0,0" VerticalAlignment="Top" Width="75" />
</Grid>
</Window>
App.xaml.cs:
public partial class App
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
new MainWindow {DataContext = new MyAppViewModel()}.Show();
}
}
MyAppViewModel:
[ImplementPropertyChanged]
public class MyAppViewModel
{
public NavigatorViewModel Navigator { get; set; }
public MyAppViewModel()
{
Navigator = new NavigatorViewModel();
Navigator.Navigate(new MainNavigatableViewModel());
}
}
App.xaml:
<DataTemplate DataType="{x:Type viewmodels:MainNavigatableViewModel}">
<controls:MainControl/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:PostEditViewModel}">
<controls:PostEditControl/>
</DataTemplate>
The downside is that you have more ViewModel-code which manages the state of what you are looking at. But obviously that is also a huge advantage in terms of Testability. And of course your ViewModels do not need to depend on your Views.
Plus I use Fody/PropertyChanged, that's what the [ImplementPropertyChanged] is about. Keeps me from writing OnPropertyChanged code.
Here is another way to implement the NavigationService, without using an abstract class and without referencing view types in your view model.
Assuming that the view model of the destination page is something like this:
public interface IDestinationViewModel { /* Interface of destination vm here */ }
class MyDestinationViewModel : IDestinationViewModel { /* Implementation of vm here */ }
Then your NavigationService can simply implement the following interface:
public interface IPageNavigationService
{
void NavigateToDestinationPage(IDestinationViewModel dataContext);
}
In your main window ViewModel you need to inject the navigator and the view model of the destination page:
class MyViewModel1 : IMyViewModel
{
public MyViewModel1(IPageNavigationService navigator, IDestinationViewModel destination)
{
GoToPageCommand = new RelayCommand(() =>
navigator.NavigateToDestinationPage(destination));
}
public ICommand GoToPageCommand { get; }
}
The implementation of the NavigationService encapsulates the view type (Page2) and the reference to the frame which is injected through the constructor:
class PageNavigationService : IPageNavigationService
{
private readonly Frame _navigationFrame;
public PageNavigationService(Frame navigationFrame)
{
_navigationFrame = navigationFrame;
}
void Navigate(Type type, object dataContext)
{
_navigationFrame.Navigate(type);
_navigationFrame.DataContext = dataContext;
}
public void NavigateToDestinationPage(IDestinationViewModel dataContext)
{
// Page2 is the corresponding view of the destination view model
Navigate(typeof(Page2), dataContext);
}
}
To get the frame simply name it in your MainPage xaml:
<Frame x:Name="RootFrame"/>
In the code behind of the MainPage initialize your bootstrapper by passing the root frame:
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
var bootstrapper = new Bootstrapper(RootFrame);
DataContext = bootstrapper.GetMainScreenViewModel();
}
}
Finally here is the bootstrapper implementation for completeness ;)
class Bootstrapper
{
private Container _container = new Container();
public Bootstrapper(Frame frame)
{
_container.RegisterSingleton(frame);
_container.RegisterSingleton<IPageNavigationService, PageNavigationService>();
_container.Register<IMyViewModel, MyViewModel1>();
_container.Register<IDestinationViewModel, IDestinationViewModel>();
#if DEBUG
_container.Verify();
#endif
}
public IMyViewModel GetMainScreenViewModel()
{
return _container.GetInstance<IMyViewModel>();
}
}
This simply bothers me that no one has solved this at the architectural level. So this is the code for complete decoupling the views, viewmodels and the mapping between them with using the built-in Frame based navigation. The implementation uses Autofact as DI container, but can be easily ported to other IoC solutions.
Core VM logic (these should be in the same assembly):
// I would not get into how the ViewModel or property change notification is implemented
public abstract class PageViewModel : ViewModel
{
protected internal INavigationService Navigation { get; internal set; }
internal void NavigationCompleted()
{
OnNavigationCompleted();
}
protected virtual void OnNavigationCompleted()
{
}
}
public interface INavigationService
{
void Navigate<TModel>() where TModel : PageViewModel;
}
public abstract class NavigationServiceBase : INavigationService
{
public abstract void Navigate<TModel>() where TModel : PageViewModel;
protected void CompleteNavigation(PageViewModel model)
{
model.Navigation = this;
model.NavigationCompleted();
}
}
This code should be in a UWP class library or executable:
public interface INavigationMap<TModel>
where TModel: PageViewModel
{
Type ViewType { get; }
}
internal class NavigationMap<TModel, TView> : INavigationMap<TModel>
where TModel: PageViewModel
where TView: Page
{
public Type ViewType => typeof(TView);
}
public class NavigationService : NavigationServiceBase
{
private readonly Frame NavigationFrame;
private readonly ILifetimeScope Resolver;
public NavigationService(ILifetimeScope scope)
{
Resolver = scope;
NavigationFrame = Window.Current.Content as Frame;
NavigationFrame.Navigated += NavigationFrame_Navigated;
}
private void NavigationFrame_Navigated(object sender, Windows.UI.Xaml.Navigation.NavigationEventArgs e)
{
if(e.Content is FrameworkElement element)
{
element.DataContext = e.Parameter;
if(e.Parameter is PageViewModel page)
{
CompleteNavigation(page);
}
}
}
public override void Navigate<TModel>()
{
var model = Resolver.Resolve<TModel>();
var map = Resolver.Resolve<INavigationMap<TModel>>();
NavigationFrame.Navigate(map.ViewType, model);
}
}
The rest is just convenience code for registering in the DI and usage examples:
public static class NavigationMap
{
public static void RegisterNavigation<TModel, TView>(this ContainerBuilder builder)
where TModel : PageViewModel
where TView : Page
{
builder.RegisterInstance(new NavigationMap<TModel, TView>())
.As<INavigationMap<TModel>>()
.SingleInstance();
}
}
builder.RegisterNavigation<MyViewModel, MyView>();
public class UserAuthenticationModel : PageViewModel
{
protected override void OnNavigationCompleted()
{
// UI is visible and ready
// navigate to somewhere else
Navigation.Navigate<MyNextViewModel>();
}
}
I'm very new to Caliburn Micro and would like to access ViewModel properties during OnExit.
public class AppBootstrapper : Bootstrapper<MainViewModel>
{
protected override void OnExit(object sender, EventArgs e)
{
if (mainViewModel.MyParam == 42)
{
}
base.OnExit(sender, e);
}
From the default WP7 template (without Caliburn) i'm used to have App.ViewModel, which is a static field with a singleton get accessor, where the viewmodel will be created on the first access.
(See next code snippet)
public partial class App : Application
{
private static MainViewModel viewModel = null;
public static MainViewModel ViewModel
{
get
{
// Delay creation of the view model until necessary
if (viewModel == null)
viewModel = new MainViewModel();
return viewModel;
}
set
{
viewModel = value;
}
}
Now i try to use Caliburn Micro 1.1 with a WPF project and don't know how this should be done.
I need access to the ViewModel during OnExit inside AppBootStrapper.
I assume, that this should be possible, because my AppBootstrapper is inherited from Bootstrapper, but can't find the right way of doing this..
Any hints, how this can be done in WPF are very welcome?
Thanks
Rob
Try
MainViewModel mainViewModel = IoC.Get<MainViewModel>();
Here is how it would look in your code:
public class AppBootstrapper : Bootstrapper<MainViewModel>
{
protected override void OnExit(object sender, EventArgs e)
{
// Get the Main View Model
MainViewModel mainViewModel = IoC.Get<MainViewModel>();
if (mainViewModel.MyParam == 42)
{
//Do work
}
base.OnExit(sender, e);
}
}
This assumes Two Things:
Your MainViewModel class is exporting typeof(MainViewModel) and not something different, like typeof(IShell)
You are using the default MEF implementation of C.M.
After searching a little bit more i think i've found the solution to my own question: Added SimpleContainer.cs from here: link
and added this to my AppBootstrapper code:
public class AppBootstrapper : Bootstrapper<MainViewModel>
{
private SimpleContainer container;
protected override void Configure()
{
container = new SimpleContainer();
container.RegisterSingleton(typeof(MainViewModel), null, typeof(MainViewModel));
container.RegisterSingleton(typeof(IWindowManager), null, typeof(WindowManager));
}
protected override object GetInstance(Type service, string key)
{
return container.GetInstance(service, key);
}
Would be great to hear some comment, whether this is ok or not.