Summary
When a button is pressed, call RequestNavigate which takes MainView from ContentRegion and inserts it into DialogContentRegion. Currently, on RequestNavigate, a new instance of MainView is inserted into DialogContentRegion.
Description
Here is a diagram that synthesizes what I'm looking for.
ContentRegion is the main region and contains the MainView.
DialogView contains the region DialogContentRegion in a ContentControl.
IDialogService is used in the PinModuleCommand of the MainViewModel to show the dialog.
In DialogViewModel, RegionManager.RequestNavigate() is used to display view in DialogContentRegion when the dialog is opened.
IContainerRegistry is used to register the views.
public void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterForNavigation<MainView>();
containerRegistry.RegisterDialog<DialogView>();
}
The same RegionManager is used throughout.
DialogView.xaml
<UserControl x:Class="Infrastructure.Views.DialogView"
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:Infrastructure.Views"
xmlns:prism="http://prismlibrary.com/" xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d">
<ContentControl x:Name="_contentControl" prism:RegionManager.RegionName="DialogContentRegion"/>
</UserControl>
DialogView.xaml.cs
public partial class DialogView : UserControl
{
IRegionManager _regionManager;
public DialogView(IRegionManager regionManager)
{
InitializeComponent();
_regionManager = regionManager;
if (!_regionManager.Regions.ContainsRegionWithName("DialogContentRegion"))
{
RegionManager.SetRegionManager(_contentControl, _regionManager);
}
}
}
DialogViewModel.cs
public class DialogViewModel : BindableBase, IDialogAware
{
IRegionManager _regionManager;
private string _title;
public string Title
{
get { return _title; }
set { SetProperty(ref _title, value); }
}
private string _view;
public string View
{
get { return _view; }
set { SetProperty(ref _view, value); }
}
public PinDialogViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
}
public event Action<IDialogResult> RequestClose;
public bool CanCloseDialog()
{
return true;
}
public void OnDialogClosed()
{
_regionManager.Regions.Remove("DialogContentRegion");
}
public void OnDialogOpened(IDialogParameters parameters)
{
View = parameters.GetValue<string>("view");
if (_regionManager.Regions.ContainsRegionWithName("DialogContentRegion"))
{
_regionManager.RequestNavigate("DialogContentRegion", View);
}
}
}
MainViewModel.cs
public class MainViewModel : BindableBase, INavigationAware
{
private DelegateCommand _pinModuleComamnd;
public DelegateCommand PinModuleCommand =>
_pinModuleComamnd ?? (_pinModuleComamnd = new DelegateCommand(ExecutePinModuleCommand));
public MainViewModel()
{
}
void ExecutePinModuleCommand()
{
_dialogService.Show(nameof(PinDialogView), new DialogParameters { { "view", "MainView" } }, DialogCallback);
}
private void DialogCallback(IDialogResult dialogResult)
{
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
}
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return true;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
}
}
Expected behavior
When navigating to MainView in the DialogContentRegion, the view instance in the dialog should be the same as in the main window.
Actual behavior
A new view instance is created.
Additional information
I have observed that on navigating in the dialog, IsNavigationTarget() is never called in MainViewModel. Why is that?
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;
}
}
Problem:
My Viewmodel creates a instance of a controller mycontroller and the property Busy shall be passed to the View and updated in the View based on the controller state, but my View doesn't get updated. The other property Busy2 is updated based on the current state. Bindablebase implements the I
Question:
Why the property Busy in the ViewModel is not updated? The mycontroller.Busy property is updating, but not the view.
Framework & Tools:
PRISM 6.3.0
Fody.Propertychanged
View:
<TextBlock Text="{Binding Path=Busy}"></TextBlock>
<TextBlock Text="{Binding Path=Busy2}"></TextBlock>
ViewModel:
public class Controller
{
public bool Busy { get; private set; }
public async void GetValue()
{
Busy = true;
await Task.Delay(5000);
Busy = false;
}
}
public class MyViewModel : BindableBase
{
private readonly Controller _mycontroller;
public DelegateCommand<string> RunCommand { get; private set; }
// This property is not updated in the view
public bool Busy
{
get { return _mycontroller.Busy; }
}
// Works as aspected
public bool Busy2 { get; set; }
public MyViewModel()
{
_mycontroller = new Controller();
RunCommand = new DelegateCommand<string>(Run, Canrun).ObservesProperty((() => _mycontroller.Busy));
}
private bool Canrun(string arg)
{
return _mycontroller.Busy != true;
}
private void Run(string obj)
{
Busy2 = true;
_mycontroller.GetValue();
}
}
Update:
I added the Bindablebase from Prism, because is implement the INotifyPropertyChanged, but the view is still not updated.
I refactored the code and I set a breakpoint to set { SetProperty(ref _busy, value); } and the breakpoint is never reached.
I removed the Propertychanged.Fody nuget package too.
ViewModel:
public class Controller : BindableBase
{
private bool _busy;
public bool Busy
{
get { return _busy; }
set { SetProperty(ref _busy, value); }
}
public Controller()
{
}
public void DoWork1()
{
for (var i = 0; i < 10; i++)
{
Thread.Sleep(1000);
_busy = !_busy;
Debug.WriteLine(_busy.ToString());
}
}
public void DoWork2()
{
_busy = !_busy;
}
}
public class MainWindowViewModel : BindableBase
{
private Controller mycontroller;
private string _title = "Prism Unity Application";
public DelegateCommand RunCommand { get; private set; }
public string Title
{
get { return _title; }
set { SetProperty(ref _title, value); }
}
public bool Busy
{
get { return mycontroller.Busy; }
}
public MainWindowViewModel()
{
RunCommand = new DelegateCommand(Execute);
mycontroller = new Controller();
}
private void Execute()
{
mycontroller.DoWork1();
}
}
View:
<Window x:Class="PropertytestPrism.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
Title="{Binding Title}" Height="350" Width="525">
<StackPanel>
<Button Command="{Binding RunCommand}" Content="Run"></Button>
<TextBlock Text="{Binding Busy}"></TextBlock>
</StackPanel>
</Window>
Update 2
Failure: Missing INotifypropertychanged for the class Controller
The View is still not updated and the reason for this is my Delegatecommandmethod, which executes mycontroller.DoWork1();
Question:
Why the View is not updated? If I execute the method inside the DelegateCommandmethod?
You should implement INotifyPropertyChanged by your Controller class. Property is changed inside Controller class and this changing should be notified:
public class Controller : INotifyPropertyChanged
{
private bool _busy;
public bool Busy
{
get
{
return _busy;
}
private set
{
SetField(ref _busy, value, "Busy"); }
}
}
public async void GetValue()
{
Busy = true;
await Task.Delay(5000);
Busy = false;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
protected bool SetField<T>(ref T field, T value, string propertyName)
{
if (EqualityComparer<T>.Default.Equals(field, value))
{
return false;
}
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
Try making your bindings work like this.
<TextBlock Text="{Binding Busy,Mode=OneWay}"></TextBlock>.
By default bindings are one time and so they wont be updated even on the property changed notification.
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;
}
}
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 have a button on my Main Window. When I click it I want another Window to popup on top of the Main Window.
Main Window is still visible and should be the parent of this new window.
I been looking around and not sure how to do it, some people suggested to use the Messenger to do this but did not really give an example.
Hi I didnt have MVVMLight so I have used custom Messanger because to make picture clearer how things work.
MessageType
public enum MessageType
{
DataLoaded,
OpenWindow,
SetFocus,
OpenExceptionWindow,
Refresh
//etc
}
Message
public class Message
{
public Message(MessageType messageType, object message)
{
MessageType = messageType;
MessageObject = message;
}
public MessageType MessageType { get; private set; }
public object MessageObject { get; private set; }
}
Messanger
public class Messanger
{
//Singleton
private Messanger()
{ }
static Messanger instance;
public static Messanger Instance
{
get{return instance ?? (instance=new Messanger());}
}
static Dictionary<string, Action<Message>> dictionary = new Dictionary<string, Action<Message>>();
//View Calls this and register the delegate corresponding to the unique token
public void Register(string token,Action<Message> action)
{
if (dictionary.ContainsKey(token))
throw new Exception("Already registered");
if (action == null)
throw new ArgumentNullException("action is null");
dictionary.Add(token, action);
}
public void UnRegister(string token)
{
if(dictionary.ContainsKey(token))
dictionary.Remove(token);
}
//ViewModel Calls this and pass the token and Message.
//the registered delegate is looked up in dictionary corresponding to that token and
//Corresponding register delegate fired.
public void SendMessage(string token,Message message)
{
if (dictionary.ContainsKey(token))
dictionary[token](message);
}
}
ViewBase
public class ViewBase:Window
{
protected string Token { get; private set; }
public ViewBase()
{
Token = Guid.NewGuid().ToString();
//Register to Messanger
Messanger.Instance.Register(Token, HandleMessages);
//UnRegister On Closing or Closed
this.Closing +=(s,e)=> Messanger.Instance.UnRegister(Token);
}
//Handle Common Messages to all Windows Here
void HandleMessages(Message message)
{
switch (message.MessageType)
{
case MessageType.OpenExceptionWindow:
Exception ex = message.MessageObject as Exception;
ExceptionWindow window = new ExceptionWindow();
window.Exception = ex;
window.ShowDialog();
break;
//other common cases should be handled here
default : HandleWindowLevelMessage(message);
break;
}
}
protected virtual void HandleWindowLevelMessage(Message message)
{
}
}
View
public partial class Mywindow : ViewBase
{
public Mywindow()
{
InitializeComponent();
DataContext = new MyViewModel(Token);
}
protected override void HandleWindowLevelMessage(Message message)
{
//open window according to OP Requirement
if (message.MessageType == MessageType.OpenWindow)
{
string windowName = message.ToString();
if (windowName != null)
{
//logic to get the window . I assume that OP have some logic to get the child window this is just temporary
var window = Application.Current.Windows.OfType<Window>().FirstOrDefault(s=>s.Name==windowName);
if (window != null)
{
window.Owner=this;
window.Show();
}
}
}
base.HandleWindowLevelMessage(message);
}
}
View.xaml Here first element is not Window now
<local:ViewBase x:Class="WpfApplication4.Mywindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication4"
Title="Mywindow" Height="300" Width="300">
<Grid>
<Button Content="ok" Click="Button_Click_1"/>
</Grid>
ViewModelBase
public class ViewModelBase : INotifyPropertyChanged
{
public ViewModelBase(string token)
{
Token = token;
}
protected string Token { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
void RaisePropertyChanged(string propName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
ViewModel
public class MyViewModel : ViewModelBase
{
public MyViewModel(string token)
: base(token)
{
}
//say OP want to open window on Command Execute
public void OnCommand()
{
Messanger.Instance.SendMessage(Token, new Message(MessageType.OpenWindow, "MyChildWindow"));
}
}
I hope this will help. This is simple code to understand feel free to ask.