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>();
}
}
Related
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?
To pass data beetwen viewmodels when navigating I use query parameters (IQueryAttributable), i.e.:
NavigationParameters[nameof(SomeProperty)] = SomeProperty;
await Shell.Current.GoToAsync("SomePage", NavigationParameters);
It works as it should be working, but I wish to put SomePage into a TabBar:
<TabBar>
<ShellContent Route="SomePage"
ContentTemplate="{DataTemplate local:SomePage}"/>
...
</TabBar>
Is there a way to pass data when user click/tap SomePage icon on the tab bar? Is there some event for that so I could hook up GoToAsync method? Or maybe there is another way than query to pass data beetwen viewmodels?
MauiProgram.cs:
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
builder.Services
.AddSingleton<Page_1>
.AddSingleton<ViewModel_1>
.AddSingleton<Page_2>
.AddSingleton<ViewModel_2>
.AddSingleton<ISharedDataInterface, SharedDataInterfaceImplementation>();
return builder.Build();
}
}
Page_1.xaml.cs:
public partial class Page_1 : ContentPage
{
public LogoPage(ViewModel_1 viewModel_1)
{
BindingContext = viewModel_1;
InitializeComponent();
}
}
Page_2.xaml.cs:
public partial class Page_2 : ContentPage
{
public LogoPage(ViewModel_2 viewModel_2)
{
BindingContext = viewModel_2;
InitializeComponent();
}
}
ViewModel_1.cs:
public class ViewModel_1
{
public ViewModel_1(ISharedDataInterface sharedDataInterfaceImplementation)
{
...
}
}
ViewModel_2.cs:
public class ViewModel_2
{
public ViewModel_2(ISharedDataInterface sharedDataInterfaceImplementation)
{
...
}
}
If the two pages are in the same Tab,you can create a static global variable for the viewmodel in App.xaml.cs and access this variable in your different pages.
For example,we can first define a view model (e.g. TestViewModel.cs):
public class TestViewModel: INotifyPropertyChanged
{
public TestViewModel() {
str = "123";
}
public string _str;
public string str
{
get
{
return _str;
}
set
{
_str = value;
OnPropertyChanged("str");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
And define a static global variable in App.xaml.cs:
public partial class App : Application
{
public static TestViewModel testViewModel;
public App()
{
InitializeComponent();
MainPage = new AppShell();
testViewModel = new TestViewModel();
}
}
Then , you can assgin the global variable for BindingContext in different pages:
public partial class Tab1 : ContentPage
{
public Tab1()
{
InitializeComponent();
BindingContext = App.testViewModel;
}
}
public partial class Tab2 : ContentPage
{
public Tab2()
{
InitializeComponent();
BindingContext = App.testViewModel;
}
private void Button_Clicked(object sender, EventArgs e)
{
App.testViewModel.str = "Test";
}
}
"You could also hold handles to other view models in your view model, like a common section that's shared between all your pages."
That is the advice to follow.
Suppose each tab's viewmodel has public MySharedType Shared { get; private set; },
and constructor is MyTab1ViewModel(MySharedType shared) { this.Shared = shared; }.
Then from xaml, access a shared property by: {Binding Shared.MyProperty1}.
Let's see a practical example.
You have one TabbedPage and two ContentPage. The idea is that you will pass the correct parameter when the user taps the specific tab. In this context you can pass any parameter on demand to your tab page.
<?xml version="1.0" encoding="utf-8" ?>
<TabbedPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyApp.Pages.TabPage"
xmlns:pages="clr-namespace:MyApp.Pages">
<pages:Page1/>
<pages:Page2/>
</TabbedPage>
Add the bellow code to the TabbedPage code behind.
public TabPage()
{
InitializeComponent();
this.CurrentPageChanged += TabPage_CurrentPageChanged;
PassParams();
}
private void TabPage_CurrentPageChanged(object sender, EventArgs e)
{
PassParams();
}
private void PassParams()
{
if (this.CurrentPage.GetType() == typeof(Page1))
{
Page1 cur = (Page1)this.CurrentPage;
cur.Param1 = 123;
}
if (this.CurrentPage.GetType() == typeof(Page2))
{
Page2 cur = (Page2)this.CurrentPage;
cur.Param2 = 456;
}
}
Add the below to Page1 code behind. About the same code goes to Page2, just replace number 1.
public Page1()
{
InitializeComponent();
}
int param1;
public int Param1
{
set
{
param1 = value;
OnPropertyChanged();
BindingContext = new Page1ViewModel(param1);
}
}
The Page1ViewModel will have to handle the parameter.
public Page1ViewModel(int param1)
{
//populate your view with your models
}
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 am working on a windows phone 8.1 universal app and want to find the best way of handling page navigations without having large amounts of logic in the code behind. I want to keep the code behind in my View as uncluttered as possible. What is the accepted MVVM way of navigating to a new page in response to a button click?
I currently have to send a RelayComnmand message from the ViewModel to the view with the details of the page to navigate to. This means that the code behind has to be wired up as follows:
public MainPage()
{
InitializeComponent();
Messenger.Default.Register<OpenArticleMessage>(this, (article) => ReceiveOpenArticleMessage(article));
...
}
private object ReceiveOpenArticleMessage(OpenArticleMessage article)
{
Frame.Navigate(typeof(ArticleView));
}
This just doesn't seem the best way although it does work. How can I do the page navigations directly from the ViewModel? I am using MVVM-Light in my project.
Ok, I have found an answer to this question. Took a bit of investigation but I eventually found the preferred MVVM-Light way of doing this. I don't take credit for this answer in anyway but just posting it here in case people are looking for an answer to this question.
Create an INavigationService interface as follows:
public interface INavigationService
{
void Navigate(Type sourcePageType);
void Navigate(Type sourcePageType, object parameter);
void GoBack();
}
Create a NavigationService class as follows:
public class NavigationService : INavigationService
{
public void Navigate(Type sourcePageType)
{
((Frame)Window.Current.Content).Navigate(sourcePageType);
}
public void Navigate(Type sourcePageType, object parameter)
{
((Frame)Window.Current.Content).Navigate(sourcePageType, parameter);
}
public void GoBack()
{
((Frame)Window.Current.Content).GoBack();
}
}
Now in the ViewModelLocator, set it up like this:
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance",
"CA1822:MarkMembersAsStatic",
Justification = "This non-static member is needed for data binding purposes.")]
public MainViewModel Main
{
get
{
return ServiceLocator.Current.GetInstance<MainViewModel>();
}
}
static ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
if (ViewModelBase.IsInDesignModeStatic)
{
SimpleIoc.Default.Register<INavigationService, Design.DesignNavigationService>();
}
else
{
SimpleIoc.Default.Register<INavigationService>(() => new NavigationService());
}
SimpleIoc.Default.Register<MainViewModel>();
}
Next setup a navigation service for design time as follows:
public class DesignNavigationService : INavigationService
{
// This class doesn't perform navigation, in order
// to avoid issues in the designer at design time.
public void Navigate(Type sourcePageType)
{
}
public void Navigate(Type sourcePageType, object parameter)
{
}
public void GoBack()
{
}
}
My MainViewModel constructor is as follows:
public MainViewModel(INavigationService navigationService)
{
_navigationService = navigationService;
...
Now you can simply use this to navigate in your viewmodel:
_navigationService.Navigate(typeof(WelcomeView));
For more details on the original author Laurent Bugnion see this article and associated code.
http://msdn.microsoft.com/en-us/magazine/jj651572.aspx
There is a new and simpler implementation here: https://marcominerva.wordpress.com/2014/10/10/navigationservice-in-mvvm-light-v5/
First we create the NavigationService and DialogService (for the page navigation params):
public ViewModelLocator() {
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
var navigationService = this.CreateNavigationService();
SimpleIoc.Default.Register<INavigationService>(() => navigationService);
SimpleIoc.Default.Register<IDialogService, DialogService>();
SimpleIoc.Default.Register<MainViewModel>();
SimpleIoc.Default.Register<DetailsViewModel>();
}
private INavigationService CreateNavigationService() {
var navigationService = new NavigationService();
navigationService.Configure("Details", typeof(DetailsPage));
// navigationService.Configure("key1", typeof(OtherPage1));
// navigationService.Configure("key2", typeof(OtherPage2));
return navigationService;
}
Then we create a RelayCommand and NavigationService in your ViewModel, like so:
public class MainViewModel : ViewModelBase {
private INavigationService _navigationService;
public RelayCommand<Tuple<string, string>> DetailsCommand { get; set; }
public MainViewModel(INavigationService navigationService) {
this._navigationService = navigationService;
DetailsCommand = new RelayCommand<Tuple<string, string>>((args) => NavigateTo(args));
}
public void NavigateTo(Tuple<string, string> args) {
this._navigationService.NavigateTo(args.Item1, args.Item1);
}
public void ClickAndNavigate() {
NavigateTo(new Tuple<string, string>("AdminPivotPage", "Test Params"));
}
}
And finally, we can get the page navigation params like so:
public sealed partial class DetailsPage : Page {
// ...
protected override void OnNavigatedTo(NavigationEventArgs e) {
var parameter = e.Parameter as string; // "My data"
base.OnNavigatedTo(e);
}
}
But to read the arguments passed in page navigation in MVVM pattern, you can take a look here.
I agree with ricochete above, its simpler, though my direct implimentation messed up with my Design Data Binding in Blend.
I decided to create an a class that inherited from the NavigationService
public class NavigationServiceHelper : NavigationService
{
public NavigationServiceHelper()
{
this.Configure("Page1", typeof(View.Page1));
this.Configure("Page2", typeof(View.Page2));
}
}
Then in the ViewModelLocator I registerred it this way
SimpleIoc.Default.Register<INavigationService, NavigationServiceHelper>();
My Design View Data Bindings worked again. If someone could explain why the design data wun't work in ricochete above, please do. Thank you!
I have a problem with listView initializations. The .xaml part of the listView is as below,
<ListView x:Name="categoryListView" HorizontalAlignment="Left" Width="129" Height="180"
ItemsSource="{Binding Path=RecordModel.CategoryList}"
DisplayMemberPath="RecordModel.CategoryList"
SelectedValue="{Binding Path=RecordModel.RecordTitle}"
VerticalAlignment="Top">
I have a list of String paths in RecordModel.CategoryList but I need to change the list at window initialization. Part of the view-model is below. Where can I add the code to change the list so the listView gets the changed list items at start?
public class MainWindowViewModel : ViewModelBase
{
...
private RecordModel _recordModel;
private ICommand _addCategoryCommand;
...
public MainWindowViewModel()
{
_recordModel = new RecordModel();
}
public RecordModel RecordModel
{
get { return _recordModel; }
set { _recordModel = value; }
}
...
public ICommand AddCategoryCommand
{
get
{
if (_addCategoryCommand == null)
_addCategoryCommand = new AddCat ();
return _addCategoryCommand;
}
}
public class AddCat : ICommand
{
public bool CanExecute(object parameter) { return true; }
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
MainWindowViewModel mainWindowViewModel = (MainWindowViewModel)parameter;
...
//Do things with mainWindowViewModel and the variables it has
}
...
This is the reason that ViewModels exist: so that they can transparently convert values from the Model to values more appropriate for binding.
You should expose a CategoryList property on the MainWindowViewModel and bind directly on that. You can then populate it by processing the values of RecordModel.CategoryList in the RecordModel property setter:
public class MainWindowViewModel : ViewModelBase
{
private RecordModel _recordModel;
public MainWindowViewModel()
{
RecordModel = new RecordModel(); // set the property not the field
}
public RecordModel RecordModel
{
get { return _recordModel; }
set {
_recordModel = value;
// populate CategoryList here from value.CategoryList
}
}
public UnknownType CategoryList { get; }
}