好的,我举了一个简单的例子,向您展示如何使用带有数据绑定的 MVVM(Model-View-ViewModel) 方法动态更改 ContentControl 的内容。
我建议您创建一个新项目并加载这些文件,看看它是如何工作的。
我们首先需要实现 INotifyPropertyChanged 接口。这将允许您定义您自己的具有属性的类,这些属性将在属性发生更改时通知 UI。我们创建一个提供此功能的抽象类。
ViewModelBase.cs
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
this.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
var handler = this.PropertyChanged;
if (handler != null)
{
handler(this, e);
}
}
}
我们现在需要数据模型。为简单起见,我创建了 2 个模型 - HomePage 和 SettingsPage。两种模型只有一个属性,您可以根据需要添加更多属性。
HomePage.cs
public class HomePage
{
public string PageTitle { get; set; }
}
SettingsPage.cs
public class SettingsPage
{
public string PageTitle { get; set; }
}
然后我创建相应的 ViewModel 来包装每个模型。请注意,视图模型继承自我的 ViewModelBase 抽象类。
HomePageViewModel.cs
public class HomePageViewModel : ViewModelBase
{
public HomePageViewModel(HomePage model)
{
this.Model = model;
}
public HomePage Model { get; private set; }
public string PageTitle
{
get
{
return this.Model.PageTitle;
}
set
{
this.Model.PageTitle = value;
this.OnPropertyChanged("PageTitle");
}
}
}
SettingsPageViewModel.cs
public class SettingsPageViewModel : ViewModelBase
{
public SettingsPageViewModel(SettingsPage model)
{
this.Model = model;
}
public SettingsPage Model { get; private set; }
public string PageTitle
{
get
{
return this.Model.PageTitle;
}
set
{
this.Model.PageTitle = value;
this.OnPropertyChanged("PageTitle");
}
}
}
现在我们需要为每个 ViewModel 提供 View。即 HomePageView 和 SettingsPageView。我为此创建了 2 个用户控件。
HomePageView.xaml
<UserControl x:Class="WpfApplication3.HomePageView"
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">
<Grid>
<TextBlock FontSize="20" Text="{Binding Path=PageTitle}" />
</Grid>
SettingsPageView.xaml
<UserControl x:Class="WpfApplication3.SettingsPageView"
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">
<Grid>
<TextBlock FontSize="20" Text="{Binding Path=PageTitle}" />
</Grid>
我们现在需要为 MainWindow 定义 xaml。我包含了 2 个按钮来帮助在 2 个“页面”之间导航。
MainWindow.xaml
<Window x:Class="WpfApplication3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication3"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate DataType="{x:Type local:HomePageViewModel}">
<local:HomePageView />
</DataTemplate>
<DataTemplate DataType="{x:Type local:SettingsPageViewModel}">
<local:SettingsPageView />
</DataTemplate>
</Window.Resources>
<DockPanel>
<StackPanel DockPanel.Dock="Left">
<Button Content="Home Page" Command="{Binding Path=LoadHomePageCommand}" />
<Button Content="Settings Page" Command="{Binding Path=LoadSettingsPageCommand}"/>
</StackPanel>
<ContentControl Content="{Binding Path=CurrentViewModel}"></ContentControl>
</DockPanel>
我们还需要一个用于 MainWindow 的 ViewModel。但在此之前,我们需要创建另一个类,以便我们可以将按钮绑定到命令。
DelegateCommand.cs
public class DelegateCommand : ICommand
{
/// <summary>
/// Action to be performed when this command is executed
/// </summary>
private Action<object> executionAction;
/// <summary>
/// Predicate to determine if the command is valid for execution
/// </summary>
private Predicate<object> canExecutePredicate;
/// <summary>
/// Initializes a new instance of the DelegateCommand class.
/// The command will always be valid for execution.
/// </summary>
/// <param name="execute">The delegate to call on execution</param>
public DelegateCommand(Action<object> execute)
: this(execute, null)
{
}
/// <summary>
/// Initializes a new instance of the DelegateCommand class.
/// </summary>
/// <param name="execute">The delegate to call on execution</param>
/// <param name="canExecute">The predicate to determine if command is valid for execution</param>
public DelegateCommand(Action<object> execute, Predicate<object> canExecute)
{
if (execute == null)
{
throw new ArgumentNullException("execute");
}
this.executionAction = execute;
this.canExecutePredicate = canExecute;
}
/// <summary>
/// Raised when CanExecute is changed
/// </summary>
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
/// <summary>
/// Executes the delegate backing this DelegateCommand
/// </summary>
/// <param name="parameter">parameter to pass to predicate</param>
/// <returns>True if command is valid for execution</returns>
public bool CanExecute(object parameter)
{
return this.canExecutePredicate == null ? true : this.canExecutePredicate(parameter);
}
/// <summary>
/// Executes the delegate backing this DelegateCommand
/// </summary>
/// <param name="parameter">parameter to pass to delegate</param>
/// <exception cref="InvalidOperationException">Thrown if CanExecute returns false</exception>
public void Execute(object parameter)
{
if (!this.CanExecute(parameter))
{
throw new InvalidOperationException("The command is not valid for execution, check the CanExecute method before attempting to execute.");
}
this.executionAction(parameter);
}
}
现在我们可以定义 MainWindowViewModel。 CurrentViewModel 是绑定到 MainWindow 上 ContentControl 的属性。当我们通过单击按钮更改此属性时,主窗口上的屏幕会发生变化。由于我在 Window.Resources 部分中定义的 DataTemplate,MainWindow 知道要加载哪个屏幕(用户控件)。
MainWindowViewModel.cs
public class MainWindowViewModel : ViewModelBase
{
public MainWindowViewModel()
{
this.LoadHomePage();
// Hook up Commands to associated methods
this.LoadHomePageCommand = new DelegateCommand(o => this.LoadHomePage());
this.LoadSettingsPageCommand = new DelegateCommand(o => this.LoadSettingsPage());
}
public ICommand LoadHomePageCommand { get; private set; }
public ICommand LoadSettingsPageCommand { get; private set; }
// ViewModel that is currently bound to the ContentControl
private ViewModelBase _currentViewModel;
public ViewModelBase CurrentViewModel
{
get { return _currentViewModel; }
set
{
_currentViewModel = value;
this.OnPropertyChanged("CurrentViewModel");
}
}
private void LoadHomePage()
{
CurrentViewModel = new HomePageViewModel(
new HomePage() { PageTitle = "This is the Home Page."});
}
private void LoadSettingsPage()
{
CurrentViewModel = new SettingsPageViewModel(
new SettingsPage(){PageTitle = "This is the Settings Page."});
}
}
最后,我们需要重写应用程序启动,以便我们可以将 MainWindowViewModel 类加载到 MainWindow 的 DataContext 属性中。
App.xaml.cs
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var window = new MainWindow() { DataContext = new MainWindowViewModel() };
window.Show();
}
}
删除 App.xaml 应用程序标记中的 StartupUri="MainWindow.xaml" 代码也是一个好主意,这样我们就不会在启动时获得 2 个 MainWindows。
请注意,DelegateCommand 和 ViewModelBase 类只能复制到新项目中并使用。
这只是一个非常简单的例子。您可以从here 和here 获得更好的想法
编辑
在您的评论中,您想知道是否可以不必为每个视图和相关的样板代码创建一个类。据我所知,答案是否定的。是的,您可以拥有一个巨大的类,但您仍然需要为每个 Property setter 调用 OnPropertyChanged。这也有不少缺点。首先,生成的类将很难维护。会有很多代码和依赖项。其次,很难使用 DataTemplates 来“交换”视图。仍然可以通过在 DataTemplates 中使用 x:Key 并在用户控件中硬编码模板绑定来实现。从本质上讲,你并没有真正让你的代码变得更短,但你会让自己变得更难。
我猜你的主要抱怨是不得不在你的视图模型中编写这么多代码来包装你的模型属性。看看T4 templates。一些开发人员使用它来自动生成他们的样板代码(即 ViewModel 类)。我个人不使用这个,我使用自定义代码sn-p快速生成viewmodel属性。
另一种选择是使用 MVVM 框架,例如 Prism 或 MVVMLight。我自己没有使用过,但我听说其中一些内置了使样板代码变得简单的功能。
还有一点需要注意的是:
如果您将设置存储在数据库中,则可以使用像实体框架这样的 ORM 框架从数据库生成模型,这意味着您剩下的就是创建视图模型和视图。