【问题标题】:MVVM Switching Between ViewsMVVM 在视图之间切换
【发布时间】:2019-09-28 13:37:09
【问题描述】:

我是 WPF 新手,所以请耐心等待。我有一个试图在 WPF 中重做的 WinForms 应用程序。在我当前的 WinForms 应用程序中,我将所有控件粘贴到一个表单中,并根据点击的按钮隐藏/显示它们,以及使用第二个表单。

我的目标:创建不同的视图以根据按下的按钮在它们之间平滑切换,而不是隐藏控件或制作单独的表单然后隐藏它们。

我目前有一个 MainWindow 视图(我的初始启动窗口),通过一个按钮,我切换到我的 CreateAccount 视图。我遇到的问题是,如何让 CreateAccount 中的按钮“返回”到 MainWindow?

我的最终目标是能够根据按钮点击在 4 个视图之间切换。

这是我的 MainWindow.xaml

<Window x:Class="MusicPlayer.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"
        xmlns:local="clr-namespace:MusicPlayer"
        xmlns:Views="clr-namespace:MusicPlayer.Views"
        xmlns:ViewModels="clr-namespace:MusicPlayer.ViewModels"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <DataTemplate x:Name="CreateAccountTemplate" DataType="{x:Type ViewModels:CreateAccountViewModel}">
            <Views:CreateAccountView DataContext="{Binding}"/>
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <Button x:Name="TestButton" Content="Button" HorizontalAlignment="Left" Margin="164,182,0,0" VerticalAlignment="Top" Height="61" Width="68" Click="CreateAccountView_Clicked"/>
        <PasswordBox HorizontalAlignment="Left" Margin="164,284,0,0" VerticalAlignment="Top" Width="120"/>
        <ContentPresenter Content="{Binding}"/>
    </Grid>
</Window>

我的 MainWindow.xaml.cs

using System;
using System.Windows;
using MusicPlayer.ViewModels;

namespace MusicPlayer {
    public partial class MainWindow : Window {
        public MainWindow() {
            InitializeComponent();
        }

        protected override void OnClosed(EventArgs e) {
            base.OnClosed(e);

            Application.Current.Shutdown();
        } //end of onClosed

        private void CreateAccountView_Clicked(object sender, RoutedEventArgs e) {
            DataContext = new CreateAccountViewModel();
        } //end of CreateAccountView_Clicked
    }
}

这是我的 CreateAccount.xaml

<UserControl x:Class="MusicPlayer.Views.CreateAccountView"
             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:Views="clr-namespace:MusicPlayer.Views"
             xmlns:ViewModels="clr-namespace:MusicPlayer.ViewModels"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
    </UserControl.Resources>
    <Grid Background="White">
        <Button Content="Button" HorizontalAlignment="Left" Margin="276,279,0,0" VerticalAlignment="Top" Height="60" Width="59" Click="Button_Click"/>
    </Grid>
</UserControl>

还有我的 CreateAccountView.xaml.cs

using System.Windows;
using System.Windows.Controls;
using MusicPlayer.ViewModels;

namespace MusicPlayer.Views {
    public partial class CreateAccountView : UserControl {
        //public static readonly DependencyProperty TestMeDependency = DependencyProperty.Register("MyProperty", typeof(string), typeof(CreateAccountView));

        public CreateAccountView() {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e) {
            DataContext = new MainWindowViewModel();
        }
    }
}

【问题讨论】:

标签: c# wpf xaml


【解决方案1】:

在我看来,您目前的尝试是正确的。您发布的代码的主要问题是 CreateAccountView.Button_Click() 处理程序无权访问它应该设置的 DataContext 属性:

private void Button_Click(object sender, RoutedEventArgs e) {
    DataContext = new MainWindowViewModel();
}

DataContext 属性属于CreateAccountView 用户控件。但是,这不是显示内容的控制上下文。因此更改 DataContext 属性的值没有任何有用的效果。 (实际上,用户控件根本不应该设置自己的DataContext 属性,因为这样做会丢弃使用该用户控件的客户端代码设置的任何上下文。)

没有足够的上下文来确切地知道您执行此操作的最佳方法是什么。我认为在 Stack Overflow 上无法提供足够的上下文。整体架构将取决于有关您的程序的太多小细节。但是,一种我认为很好的解决方法是:

  • 创建管理应用整体行为的“主”视图模型
  • 创建与 UI 的不同状态相关的单独视图模型
  • 让主视图模型配置各个视图模型,以根据用户输入(例如单击按钮)适当地切换当前视图模型

把它翻译成代码,看起来像这样……

首先,视图模型:

class MainViewModel : NotifyPropertyChangedBase
{
    private object _currentViewModel;
    public object CurrentViewModel
    {
        get => _currentViewModel;
        set => _UpdateField(ref _currentViewModel, value);
    }

    private readonly HomeViewModel _homeViewModel;
    private readonly Sub1ViewModel _sub1ViewModel;
    private readonly Sub2ViewModel _sub2ViewModel;

    public MainViewModel()
    {
        _sub1ViewModel = new Sub1ViewModel
        {
            BackCommand = new DelegateCommand(() => CurrentViewModel = _homeViewModel)
        };

        _sub2ViewModel = new Sub2ViewModel
        {
            BackCommand = new DelegateCommand(() => CurrentViewModel = _homeViewModel)
        };

        _homeViewModel = new HomeViewModel
        {
            ShowSub1Command = new DelegateCommand(() => CurrentViewModel = _sub1ViewModel),
            ShowSub2Command = new DelegateCommand(() => CurrentViewModel = _sub2ViewModel)
        };

        CurrentViewModel = _homeViewModel;
    }
}

class HomeViewModel : NotifyPropertyChangedBase
{
    private ICommand _showSub1Command;
    public ICommand ShowSub1Command
    {
        get => _showSub1Command;
        set => _UpdateField(ref _showSub1Command, value);
    }

    private ICommand _showSub2Command;
    public ICommand ShowSub2Command
    {
        get => _showSub2Command;
        set => _UpdateField(ref _showSub2Command, value);
    }
}

class Sub1ViewModel : NotifyPropertyChangedBase
{
    private ICommand _backCommand;
    public ICommand BackCommand
    {
        get => _backCommand;
        set => _UpdateField(ref _backCommand, value);
    }
}

class Sub2ViewModel : NotifyPropertyChangedBase
{
    private ICommand _backCommand;
    public ICommand BackCommand
    {
        get => _backCommand;
        set => _UpdateField(ref _backCommand, value);
    }
}

当然,这些视图模型只包含处理 UI 切换所需的实现细节。在您的程序中,每个还包括特定于您需要的每个视图状态的内容。

在我的小示例中,“主页”视图包含几个按钮,用于选择可用的各个子视图:

<UserControl x:Class="WpfApp1.HomeView"
             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="450" d:DesignWidth="800">
  <StackPanel Orientation="Horizontal">
    <TextBlock Text="Home: "/>
    <Button Content="Sub1" Command="{Binding ShowSub1Command}"/>
    <Button Content="Sub2" Command="{Binding ShowSub2Command}"/>
  </StackPanel>
</UserControl>

子视图只包含返回主视图所需的按钮:

<UserControl x:Class="WpfApp1.Sub1View"
             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="450" d:DesignWidth="800">
  <StackPanel Orientation="Horizontal">
    <TextBlock Text="Sub1 View: "/>
    <Button Content="Back" Command="{Binding BackCommand}"/>
  </StackPanel>
</UserControl>

<UserControl x:Class="WpfApp1.Sub2View"
             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="450" d:DesignWidth="800">
  <StackPanel Orientation="Horizontal">
    <TextBlock Text="Sub2 View: "/>
    <Button Content="Back" Command="{Binding BackCommand}"/>
  </StackPanel>
</UserControl>

最后,主窗口设置主视图模型,并声明要用于每个特定子视图的模板:

<Window x:Class="WpfApp1.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"
        xmlns:l="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
  <Window.DataContext>
    <l:MainViewModel/>
  </Window.DataContext>
  <Window.Resources>
    <DataTemplate DataType="{x:Type l:HomeViewModel}">
      <l:HomeView/>
    </DataTemplate>
    <DataTemplate DataType="{x:Type l:Sub1ViewModel}">
      <l:Sub1View/>
    </DataTemplate>
    <DataTemplate DataType="{x:Type l:Sub2ViewModel}">
      <l:Sub2View/>
    </DataTemplate>
  </Window.Resources>
  <StackPanel>
    <ContentControl Content="{Binding CurrentViewModel}"/>
  </StackPanel>
</Window>

重要的是,您会看到 none 的视图对象包含任何代码隐藏。当您以这种方式处理问题时,没有必要这样做,至少不是为了控制代码中的基本行为。 (您可能仍然会使用视图对象的代码隐藏,但这通常只是为了实现该视图对象特有的特定 用户界面 行为,而不是处理视图模型状态。)

使用这种方法,您可以让 WPF 尽可能多地完成繁重的工作。它还将所有视图模型对象彼此分离。有一个清晰的层次结构:只有顶级的“主”视图模型甚至知道其他视图模型。这允许在其他场景中根据需要重用子视图模型(“home”、“sub1”和“sub2”),而无需在其中进行任何修改或特殊情况处理。


以下是我在上面使用的辅助类:

class NotifyPropertyChangedBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void _UpdateField<T>(ref T field, T newValue,
        Action<T> onChangedCallback = null,
        [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, newValue))
        {
            return;
        }

        T oldValue = field;

        field = newValue;
        onChangedCallback?.Invoke(oldValue);
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
class DelegateCommand : ICommand
{
    private readonly Action _execute;

    public DelegateCommand(Action execute)
    {
        _execute = execute;
    }

#pragma warning disable 67
    public event EventHandler CanExecuteChanged;
#pragma warning restore

    public bool CanExecute(object parameter) => true;

    public void Execute(object parameter) => _execute();
}

【讨论】:

  • 我同意@Peter 的解决方案。我对这种方法有类似的想法。主要问题是您尝试从用户控件内部添加 MainViewModel 数据上下文,并且每次加载子控件时都会创建一个新上下文。最好的办法是拥有上面解决方案中提到的 MainViewModel 和子视图模型。
  • 这正是我想要的!谢谢你的解释!
猜你喜欢
  • 1970-01-01
  • 2011-08-11
  • 1970-01-01
  • 2011-01-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-01
相关资源
最近更新 更多