UserControl
作为View用于演示 <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>
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";
}
<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
public class MainViewModel
{
public List<PageViewModelBase> ViewModels { get; }
public MainViewModel(Page1ViewModel p1, Page2ViewModel p2, Page3ViewModel p3)
{
ViewModels = new List<PageViewModelBase> { p1, p2, p3 };
}
}
<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的时候就有了智能提示。
class NavigationService
{
//设置一个单例服务
public static NavigationService Instance { get; private set; } = new NavigationService();
//声明一个事件,当更改CurrentViewModel时触发
public event Action? CurrentViewModelChanged;
//设置一个当前VM的属性,并在属性改变时触发CurrentViewModelChanged
private ViewModelBase? currentViewModel;
public ViewModelBase? CurrentViewModel
{
get => currentViewModel;
set
{
currentViewModel = value;
CurrentViewModelChanged?.Invoke();
}
}
//页面导航方法,给CurrentViewModel赋值,触发CurrentViewModelChanged事件
public void NavigateTo(ViewModelBase viewModel)=>CurrentViewModel = viewModel;
}
public class ViewModelBase : ObservableObject{}
public partial class MainViewModel : ViewModelBase
{
[ObservableProperty]
private ViewModelBase? currentViewModel;//当前的VM
public MainViewModel()
{
//为事件绑定委托方法,设置CurrentVM和NavigationService中的CurrentVM保持一致
NavigationService.Instance.CurrentViewModelChanged += () =>
{
CurrentViewModel = NavigationService.Instance.CurrentViewModel;
};
//调用导航方法
NavigationService.Instance.NavigateTo(new LoginViewModel());
}
}
其他两个ViewModel分别为
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());
}
}
<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进行了绑定。
public void NavigateTo<T>() where T : ViewModelBase
=> CurrentViewModel = App.Current.Services.GetService<T>();
//在调用导航方法时可以使用
navigationService.NavigateTo<HomeViewModel>();
实现上一章节的功能,这种方法本质上是通过View来自动绑定VM。
public enum ApplicationPage
{
Empty,
Login,
Home
}
public class ViewModelBase : ObservableObject{}
public partial class MainViewModel : ViewModelBase
{
//MainViewModel中的CurrentPage是一个枚举类型
[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,所以在此处使用泛型
public abstract class BasePage<VM> : UserControl where VM : ViewModelBase, new()
{
public BasePage()
{
DataContext = new VM();
}
}
将Home.xaml.cs中的继承删掉,以为它和Home.xaml相互为分部类,只在一个分部类上实现继承就可以。
<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页面方法相同
<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">LoginTextBlock>
<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
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
<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服务
//封装服务
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);
}
实现上一章节功能,本质上是使用依赖注入的方式将View和ViewModel进行绑定,并利用Frame的自带的Navigate方法进行导航
public class ViewModelBase : ObservableObject{}
public partial class MainWindowViewModel : ViewModelBase
{
private readonly NavigationService navigationService;
//依赖注入
public MainWindowViewModel(NavigationService navigationService)
{
this.navigationService = navigationService;
}
[RelayCommand]
void Loaded()
{ //navigationService实现的导航方法
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实现的导航方法,此处进行了传参
navigationService.Navigate<HomeViewModel>(new Dictionary<string, object?>
{
[nameof(HomeViewModel.UserName)] = UserName
});
}
}
主窗口,使用Behaviors实现mvvm模式
<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>
主窗口后台类
public partial class MainWindow : Window
{
public MainWindow(MainWindowViewModel viewModel,Frame frame)
{
InitializeComponent();
DataContext = viewModel;
AddChild(frame);
}
}
其他View
<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">LoginTextBlock>
<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
public Home(HomeViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
public Login(LoginViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
public class NavigationService
{
//注册了单例的Frame
private readonly Frame? mainFrame;
public NavigationService(Frame? frame)
{
mainFrame = frame;
//要使用LoadCompleted事件
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);
}
}
//根据VM类型查找View,要注意VM和View的命名规范
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;
//利用Frame的Navigate方法进行导航和传参
mainFrame?.Navigate(page,extraData);
}
}
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();
}
}