I did a project in WPF MVVM. Just a single Window
with single Frame
control and few Pages. My problem was communication between pages without not violating MVVM principles. I'd like to use all bestprogramming-practices.
Can you check if this code agrees with the all best principles?
My solution has two projects: WPF client, ViewModels PCL. I want to have my ViewModels separated from the Views.
Here is the code for WPF Client:
App.xaml.cs
namespace NaviWPFApp
{
using System.Windows;
using NaviWPFApp.Views;
using NaviWPFApp.Views.Pages;
public partial class App : Application
{
public static NavigationService Navigation;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
MainWindow mainWindow = new MainWindow();
mainWindow.Show();
Navigation = new NavigationService(mainWindow.MyFrame);
Navigation.Navigate<FirstPage>();
}
}
}
App.xaml It's just:
<Application x:Class="NaviWPFApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:NaviWPFApp"
x:Name="Application">
<Application.Resources>
<local:ViewModelLocator x:Key="ViewModelLocator"/>
</Application.Resources>
</Application>
I have one main window with frame, and two very similar pages (no code-behind):
<Window x:Class="NaviWPFApp.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="NaviWPFApp" Height="300" Width="300">
<Grid>
<Frame x:Name="MyFrame" Margin="10" />
</Grid>
</Window>
<Page x:Class="NaviWPFApp.Views.Pages.FirstPage"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300" Title="FirstPage"
DataContext="{Binding FirstPageViewModel, Source={StaticResource ViewModelLocator}}">
<Grid>
<Button Command="{Binding Go2}" Height="30" Content="Go to second page" />
</Grid>
</Page>
<Page x:Class="NaviWPFApp.Views.Pages.SecondPage"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
Title="SecondPage"
DataContext="{Binding Path=SecondPageViewModel, Source={StaticResource ViewModelLocator}}">
<Grid>
<Button Command="{Binding Go1}" Content="Go back to page 1" Height="30" />
</Grid>
</Page>
I my client I have also two additional classes ViewModelLocator and NavigationService - used for naviation between pages:
namespace NaviWPFApp
{
using NaviWPFApp.ViewModels.Pages;
public class ViewModelLocator
{
public FirstPageViewModel FirstPageViewModel => new FirstPageViewModel(App.Navigation);
public SecondPageViewModel SecondPageViewModel => new SecondPageViewModel(App.Navigation);
}
}
namespace NaviWPFApp
{
using System;
using System.Linq;
using System.Reflection;
using System.Windows.Controls;
using NaviWPFApp.ViewModels.Common;
public class NavigationService : INavigationService
{
readonly Frame frame;
public NavigationService(Frame frame)
{
this.frame = frame;
}
public void GoBack()
{
frame.GoBack();
}
public void GoForward()
{
frame.GoForward();
}
public bool Navigate(string page)
{
var type = Assembly.GetExecutingAssembly().GetTypes().SingleOrDefault(a => a.Name.Equals(page));
if (type == null) return false;
var src = Activator.CreateInstance(type);
return frame.Navigate(src);
}
public bool Navigate<T>(object parameter = null)
{
var type = typeof(T);
return Navigate(type, parameter);
}
public bool Navigate(Type source, object parameter = null)
{
var src = Activator.CreateInstance(source);
return frame.Navigate(src, parameter);
}
}
}
Here is my ViewModels (Portable) project:
It's only two ViewModel classes for each Page in UI, INavigationService
(I don't want to know anything about NavigationService
implementation and UI client), MyObservableObject
and MyCommand
.
MyObservableObject
and MyCommand
are typical implementations of INotifyPropertyChanged
and ICommand
interfaces.
This is an interface and two viewmodels:
public interface INavigationService
{
void GoForward();
void GoBack();
bool Navigate(string page);
}
public class FirstPageViewModel : MyObservableObject
{
private readonly INavigationService navigationService;
public FirstPageViewModel(INavigationService navigationService)
{
this.navigationService = navigationService;
}
public MyCommand Go2
{
get { return new MyCommand(x => navigationService.Navigate("SecondPage")); }
}
}
public class SecondPageViewModel : MyObservableObject
{
private readonly INavigationService navigationService;
public SecondPageViewModel(INavigationService navigationService)
{
this.navigationService = navigationService;
}
public MyCommand Go1
{
get { return new MyCommand(x => navigationService.Navigate("FirstPage")); }
}
}
My biggest concern is this: navigationService.Navigate("FirstPage"));
I pass view name as a string. That's because I don't want my ViewModel knows anything about View. But my navigation service HAS TO KNOW about View. That's why I did that interface with string parameter.
What do you think?