WPF中在MVVM模式下实现导航功能
一、利用TabControl
使用场景:项目小,不用考虑内存开销的问题。

实现方式1-手动指定ViewModel
- 分别定义3个
UserControl
作为View用于演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <UserControl ...> <Grid> <StackPanel Orientation="Vertical"> <TextBlock HorizontalAlignment="Center" VerticalAlignment="Top" Text="Page 1" /> <TextBlock d:Text="Page 1" FontSize="50" Text="{Binding PageMessage}" /> </StackPanel> </Grid> </UserControl>
|
- 分别定义ViewModel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public abstract class PageViewModelBase { public string? Header { get; set; } } public class MainViewModel { public List<PageViewModelBase> ViewModels { get; }
public MainViewModel(Page1ViewModel p1, Page2ViewModel p2, Page3ViewModel p3) { ViewModels = new List<PageViewModelBase> { p1, p2, p3 }; } } public class Page1ViewModel : PageViewModelBase { public Page1ViewModel() => Header = "Page 1";
public string PageMessage { get; set; } = "Hello, Page 1"; }
public class Page2ViewModel : PageViewModelBase { public Page2ViewModel() => Header = "Page 2";
public string PageMessage { get; set; } = "Hello, Page 2"; }
public class Page3ViewModel : PageViewModelBase { public Page3ViewModel() => Header = "Page 3";
public string PageMessage { get; set; } = "Hello, Page 3"; }
|
- 在MainWindow上定义Tabcontrol
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <Window ...> <Grid> <TabControl ItemsSource="{Binding ViewModels}"> <TabItem Header="Pag1"> <view:Page1> <view:Page1.DataContext> <local:Page1ViewModel /> </view:Page1.DataContext> </view:Page1> </TabItem> <TabItem Header="Pag2"> <view:Page1> <view:Page1.DataContext> <local:Page2ViewModel /> </view:Page1.DataContext> </view:Page1> </TabItem> <TabItem Header="Pag3"> <view:Page1> <view:Page1.DataContext> <local:Page3ViewModel /> </view:Page1.DataContext> </view:Page1> </TabItem> </TabControl> </Grid> </Window>
|
这种方式需要手动指定每个View的ViewModel
实现方式2-利用ItemTemplate
- 在MainViewModel中声明一个ViewModel列表
1 2 3 4 5 6 7 8 9
| public class MainViewModel { public List<PageViewModelBase> ViewModels { get; }
public MainViewModel(Page1ViewModel p1, Page2ViewModel p2, Page3ViewModel p3) { ViewModels = new List<PageViewModelBase> { p1, p2, p3 }; } }
|
- 在MainWindow中为TabControl指定ItemTemplate,上一步声明的ViewModel列表作为 TabControl 的 ItemsSource;为 TabControl.Resources 添 加多个 DataTemplate,指定 VM 对应什么样的 Page
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <Window d:DataContext="{d:DesignInstance Type=local:MainViewModel}" ....> <Grid> <TabControl ItemsSource="{Binding ViewModels}"> <TabControl.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding Header}"/> </DataTemplate> </TabControl.ItemTemplate> <TabControl.Resources> <DataTemplate DataType="{x:Type local:Page1ViewModel}"> <view:Page1/> </DataTemplate> <DataTemplate DataType="{x:Type local:Page2ViewModel}"> <view:Page2/> </DataTemplate> <DataTemplate DataType="{x:Type local:Page3ViewModel}"> <view:Page3/> </DataTemplate> </TabControl.Resources> </TabControl> </Grid> </Window>
|
这样的好处是自动会为不同的View绑定了相应的ViewModel。
小技巧:在xaml中加上了d:DataContext="{d:DesignInstance Type=local:MainViewModel}
,这样在写Binding的时候就有了智能提示。
以上两种方式均可结合依赖注入的方式来实现
二、自定义NavigationService服务

- 实现一个NavigationService服务,并作为单例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class NavigationService { public static NavigationService Instance { get; private set; } = new NavigationService(); public event Action? CurrentViewModelChanged; private ViewModelBase? currentViewModel; public ViewModelBase? CurrentViewModel { get => currentViewModel; set { currentViewModel = value; CurrentViewModelChanged?.Invoke(); } } public void NavigateTo(ViewModelBase viewModel)=>CurrentViewModel = viewModel; }
|
- 设置MainViewModel中的CurrentViewModel属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class ViewModelBase : ObservableObject{} public partial class MainViewModel : ViewModelBase { [ObservableProperty] private ViewModelBase? currentViewModel;
public MainViewModel() { NavigationService.Instance.CurrentViewModelChanged += () => { CurrentViewModel = NavigationService.Instance.CurrentViewModel; };
NavigationService.Instance.NavigateTo(new LoginViewModel()); } }
|
其他两个ViewModel分别为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public partial class LoginViewModel : ViewModelBase { [ObservableProperty] string? userName = "Sean"; [RelayCommand] void Login() { NavigationService.Instance.NavigateTo(new HomeViewModel()); } } public partial class HomeViewModel : ViewModelBase { [ObservableProperty] string? userName; [RelayCommand] void Logout() { NavigationService.Instance.NavigateTo(new LoginViewModel()); } }
|
- 使用ContentControl作为MainWindow上不同页面载体显示内容,并借助DataTemplate来实现View和ViewModel的映射
1 2 3 4 5 6 7 8 9 10 11 12
| <Window ...> <ContentControl Content="{Binding CurrentViewModel}"> <ContentControl.Resources> <DataTemplate DataType="{x:Type vm:LoginViewModel}"> <view:Login /> </DataTemplate> <DataTemplate DataType="{x:Type vm:HomeViewModel}"> <view:Home /> </DataTemplate> </ContentControl.Resources> </ContentControl> </Window>
|
在ContentControl.Resources中设置DataTemplate,根据DataType自动选择相应的VM,这样做的好处是会自动将View和VM进行了绑定。
改进
- 单例方式可以采用依赖注入的方式来实现
- 在NavigationService服务中,可以改进页面导航的方法
1 2 3 4 5
| public void NavigateTo<T>() where T : ViewModelBase => CurrentViewModel = App.Current.Services.GetService<T>();
navigationService.NavigateTo<HomeViewModel>();
|
三、借助ValueConverter
实现上一章节的功能,这种方法本质上是通过View来自动绑定VM。
- 定义Page的枚举
1 2 3 4 5 6
| public enum ApplicationPage { Empty, Login, Home }
|
- 定义各ViewModel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public class ViewModelBase : ObservableObject{} public partial class MainViewModel : ViewModelBase { [ObservableProperty] ApplicationPage currentPage;
public MainViewModel() { CurrentPage = ApplicationPage.Login; } } public partial class LoginViewModel : ViewModelBase { public string UserName { get; set; } = "AngelSix";
[RelayCommand] void Login() { var mainVM= App.Current.MainWindow.DataContext as MainViewModel; mainVM!.CurrentPage = ApplicationPage.Home; } } public partial class HomeViewModel : ViewModelBase { [RelayCommand] void Logout() { var mainVM = App.Current.MainWindow.DataContext as MainViewModel; mainVM!.CurrentPage = ApplicationPage.Login; } }
|
定义Page基类和各个Page
这种方法本质上是通过View来自动绑定VM,所以在此处使用泛型
1 2 3 4 5 6 7
| public abstract class BasePage<VM> : UserControl where VM : ViewModelBase, new() { public BasePage() { DataContext = new VM(); } }
|
将Home.xaml.cs中的继承删掉,以为它和Home.xaml相互为分部类,只在一个分部类上实现继承就可以。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <local:BasePage x:TypeArguments="vm:HomeViewModel" ...> <Grid> <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="Home" FontSize="32" /> <Button Margin="10" Grid.Row="1" HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Logout" Command="{Binding LogoutCommand}" /> </Grid> </local:BasePage>
|
方法和实现Home页面方法相同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <local:BasePage x:TypeArguments="vm:LoginViewModel" ...> <Grid> <Border Padding="10" HorizontalAlignment="Center" VerticalAlignment="Center" BorderBrush="LightGray" BorderThickness="1" CornerRadius="10"> <StackPanel Width="300"> <TextBlock HorizontalAlignment="Center" FontSize="28">Login</TextBlock> <Separator Margin="0,10" /> <TextBlock>User name:</TextBlock> <TextBox Margin="0,10" InputMethod.IsInputMethodEnabled="False" Text="{Binding UserName}" /> <TextBlock>Password:</TextBlock> <PasswordBox Margin="0,10" Password="123456" /> <Button Command="{Binding LoginCommand}" Content="Login" /> </StackPanel> </Border> </Grid> </local:BasePage>
|
定义PageViewConverter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class PageViewConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { switch ((ApplicationPage)value) { case ApplicationPage.Empty: return new TextBlock { Text = "404 Not Found" }; case ApplicationPage.Login: return new Login(); case ApplicationPage.Home: return new Home(); default: throw new ArgumentException("Invalid value passed to ApplicationPageViewConverter"); } }
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } }
|
完成MainWindow
1 2 3 4 5 6 7 8 9
| <Window ...> <Window.DataContext> <local:MainViewModel/> </Window.DataContext> <Window.Resources> <share:PageViewConverter x:Key="pageConv"/> </Window.Resources> <ContentControl Content="{Binding CurrentPage,Converter={StaticResource pageConv}}"/> </Window>
|
改进
可以结合依赖注入的方式来实现
导航方法可以封装为一个NavigationService服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| class NavigationService { public static NavigationService Instance { get; } = new NavigationService();
private MainViewModel mainVM;
public void Navigate(ApplicationPage page) { if (mainVM == null) { mainVM = (MainViewModel)App.Current.MainWindow.DataContext; }
mainVM.CurrentPage = page; } }
void Logout() { var mainVM = App.Current.MainWindow.DataContext as MainViewModel; mainVM!.CurrentPage = ApplicationPage.Login; }
void Login() { NavigationService.Instance.Navigate(ApplicationPage.Login); }
|
四、使用Frame和NavigationService
实现上一章节功能,本质上是使用依赖注入的方式将View和ViewModel进行绑定,并利用Frame的自带的Navigate方法进行导航
- 定义ViewModel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| public class ViewModelBase : ObservableObject{}
public partial class MainWindowViewModel : ViewModelBase { private readonly NavigationService navigationService; public MainWindowViewModel(NavigationService navigationService) { this.navigationService = navigationService; }
[RelayCommand] void Loaded() { navigationService.Navigate<LoginViewModel>(); } }
public partial class HomeViewModel : ViewModelBase { [ObservableProperty] string? userName; }
public partial class LoginViewModel : ViewModelBase { private readonly NavigationService navigationService; public string UserName { get; set; } = "Sergio";
public LoginViewModel(NavigationService navigationService) { this.navigationService = navigationService; }
[RelayCommand] void Login() { navigationService.Navigate<HomeViewModel>(new Dictionary<string, object?> { [nameof(HomeViewModel.UserName)] = UserName }); } }
|
- 定义View
主窗口,使用Behaviors实现mvvm模式
1 2 3 4 5 6 7 8
| <Window xmlns:b="http://schemas.microsoft.com/xaml/behaviors"> <b:Interaction.Triggers> <b:EventTrigger> <b:InvokeCommandAction Command="{Binding LoadedCommand}" /> </b:EventTrigger> </b:Interaction.Triggers> </Window>
|
主窗口后台类
1 2 3 4 5 6 7 8 9
| public partial class MainWindow : Window { public MainWindow(MainWindowViewModel viewModel,Frame frame) { InitializeComponent(); DataContext = viewModel; AddChild(frame); } }
|
其他View
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| <Page ...> <Grid> <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" d:Text="Hello, world!" Text="{Binding UserName, StringFormat='Hello, {0}!'}" FontSize="32" /> </Grid> </Page>
<Page ...> <Grid> <Border Padding="10" HorizontalAlignment="Center" VerticalAlignment="Center" BorderThickness="1" CornerRadius="10" BorderBrush="LightGray"> <StackPanel Width="300"> <TextBlock HorizontalAlignment="Center" FontSize="28">Login</TextBlock> <Separator Margin="0,10" /> <TextBlock>User name:</TextBlock> <TextBox Margin="0,10" Text="{Binding UserName}" InputMethod.IsInputMethodEnabled="False" /> <TextBlock>Password:</TextBlock> <PasswordBox Margin="0,10" Password="123456" /> <Button Content="Login" Command="{Binding LoginCommand}" /> </StackPanel> </Border> </Grid> </Page>
|
在后台类中使用依赖注入的方式定义DataContext
1 2 3 4 5 6 7 8 9 10 11
| public Home(HomeViewModel viewModel) { InitializeComponent(); DataContext = viewModel; }
public Login(LoginViewModel viewModel) { InitializeComponent(); DataContext = viewModel; }
|
- 实现NavigationService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| public class NavigationService { private readonly Frame? mainFrame;
public NavigationService(Frame? frame) { mainFrame = frame; mainFrame.LoadCompleted += MainFrame_LoadCompleted; }
private void MainFrame_LoadCompleted(object sender, System.Windows.Navigation.NavigationEventArgs e) { if (e.ExtraData is not Dictionary<string,object?> extraData) { return; } if ((mainFrame?.Content as FrameworkElement)?.DataContext is not ViewModelBase vm) { return; } foreach (var item in extraData) { vm.GetType().GetProperty(item.Key)?.SetValue(vm, item.Value); } } private Type? FindView<T>() { return Assembly.GetAssembly(typeof(T))?.GetTypes().FirstOrDefault(x => x.Name == typeof(T).Name.Replace("ViewModel", "")); } public void Navigate<T>(Dictionary<string,object?>? extraData=null) where T:ViewModelBase { var viewType = FindView<T>(); if (viewType is null) return; var page = App.Current.Services.GetService(viewType) as Page; mainFrame?.Navigate(page,extraData); } }
|
- 注册需要的类,此案例在App.cs中进行注册
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| public partial class App : Application { public IServiceProvider Services { get; }
public static new App Current => (App)Application.Current;
public App() { var container = new ServiceCollection();
container.AddSingleton(_ => new Frame { NavigationUIVisibility = NavigationUIVisibility.Hidden });
container.AddSingleton<MainWindow>(); container.AddSingleton<MainWindowViewModel>();
container.AddTransient<Login>(); container.AddTransient<Home>();
container.AddTransient<LoginViewModel>(); container.AddTransient<HomeViewModel>();
container.AddSingleton<NavigationService>();
Services = container.BuildServiceProvider(); }
protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e);
MainWindow = Services.GetRequiredService<MainWindow>(); MainWindow.Show(); } }
|