简体   繁体   English

MVVM 在视图之间切换

[英]MVVM Switching Between Views

I'm new to WPF so bear with me.我是 WPF 的新手,所以请耐心等待。 I have a WinForms Application that I am trying to redo in WPF.我有一个 WinForms 应用程序,我试图在 WPF 中重做。 In my current WinForms Application, I stick all my Controls into one Form, and hide/show them based on what buttons are hit, as well as making use of a second form.在我当前的 WinForms 应用程序中,我将所有控件粘贴到一个表单中,并根据点击的按钮隐藏/显示它们,以及使用第二个表单。

My goal: Create different views to switch between smoothly based on what button is hit, instead of hiding Controls or making separate Forms and then hiding those.我的目标:创建不同的视图以根据按下的按钮平滑切换,而不是隐藏控件或制作单独的 Forms 然后隐藏它们。

I currently have a MainWindow view (My initial launch window), where with a button, I switch to my CreateAccount view.我目前有一个 MainWindow 视图(我的初始启动窗口),通过一个按钮,我切换到我的 CreateAccount 视图。 What I am having issues with is, how can I make my button in my CreateAccount go "back" to my MainWindow?我遇到的问题是,如何将 CreateAccount go 中的按钮“返回”到主窗口?

My end goal is to be able to switch between 4 views based off Button clicks.我的最终目标是能够根据按钮点击在 4 个视图之间切换。

Here is my MainWindow.xaml这是我的 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>

My MainWindow.xaml.cs我的 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
    }
}

And here is my CreateAccount.xaml这是我的 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>

And my CreateAccountView.xaml.cs还有我的 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();
        }
    }
}

我的项目的当前结构

It seems to me that your current attempt is on the right track.在我看来,你目前的尝试是在正确的轨道上。 The main issue with the code you posted is that the CreateAccountView.Button_Click() handler doesn't have access to the DataContext property it should be setting:您发布的代码的主要问题是CreateAccountView.Button_Click()处理程序无权访问它应该设置的DataContext属性:

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

This DataContext property belongs to the CreateAccountView user control.DataContext属性属于CreateAccountView用户控件。 However, this is not the controlling context for what's being displayed.但是,这不是显示内容的控制上下文。 So changing the value of that DataContext property doesn't have any useful effect.因此,更改该DataContext属性的值没有任何有用的效果。 (Indeed, a user control should not set its own DataContext property at all, because doing so discards whatever context the client code using that user control had set.) (实际上,用户控件根本不应该设置它自己的DataContext属性,因为这样做会丢弃使用该用户控件的客户端代码设置的任何上下文。)

There's not enough context to know exactly what the best way for you to do this would be.没有足够的上下文来确切地知道您执行此操作最佳方法是什么。 I don't think it would be possible to provide enough context here on Stack Overflow.我认为在 Stack Overflow 上无法提供足够的上下文。 The overall architecture will depend on too many little details about your program.整体架构将取决于有关您的程序的太多小细节。 But, one way to approach this which I think is a good one would be this:但是,我认为一种很好的解决方法是:

  • Create a "main" view model that governs the overall behavior of the app创建管理应用程序整体行为的“主”视图 model
  • Create individual view models that relate to different states of the UI创建与 UI 的不同状态相关的单个视图模型
  • Have the main view model configure the individual view models to switch the current view model as appropriate, given the user input (eg clicking buttons)让主视图 model 配置各个视图模型以根据用户输入(例如单击按钮)切换当前视图 model

Translating that into code, looks something like this…把它翻译成代码,看起来像这样……

First, the view models:首先,视图模型:

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);
    }
}

Of course, these view models contain only the implementation details needed to handle the UI switching.当然,这些视图模型包含处理 UI 切换所需的实现细节。 In your program, each would also include the stuff specific to each view state that you need.在您的程序中,每个还包括特定于您需要的每个视图 state 的内容。

In my little sample, the "home" view contains a couple of buttons, used to select the individual sub-views available:在我的小示例中,“主页”视图包含几个按钮,用于 select 可用的各个子视图:

<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>

The sub views just contain the button required to go back to the home view:子视图只包含 go 返回主视图所需的按钮:

<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>

Finally, the main window sets the main view model, and declares templates to use for each of the specific sub views:最后,主 window 设置主视图 model,并声明要用于每个特定子视图的模板:

<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>

Importantly, you'll see that none of the view objects include any code-behind.重要的是,您会看到没有任何视图对象包含任何代码隐藏。 It's not necessary when you approach the problem this way, at least not for the purpose of controlling basic behaviors in the code.当您以这种方式处理问题时,没有必要这样做,至少不是为了控制代码中的基本行为。 (You may still wind up with code-behind for view objects, but this will usually only be for the purpose of implementing specific user-interface behaviors unique to that view object, not for dealing with the view model state.) (您可能仍会使用视图对象的代码隐藏,但这通常仅用于实现该视图 object 独有的特定用户界面行为,而不是用于处理视图 model Z9ED39E2EA9314286B6A985A673E)

Using this approach, you let WPF do as much of the heavy-lifting as possible.使用这种方法,您可以让 WPF 尽可能多地完成繁重的工作。 It also decouples all of the view model objects from each other.它还将所有视图 model 对象彼此分离。 There's a clear hierarchy: only the top-level "main" view model even knows about the other view models.有一个清晰的层次结构:只有顶级“主”视图 model 甚至知道其他视图模型。 This allows the sub-view models ("home", "sub1", and "sub2") to be reused as necessary in other scenarios without any modification or special-case handling within them.这允许在其他场景中根据需要重用子视图模型(“home”、“sub1”和“sub2”),而无需在其中进行任何修改或特殊情况处理。


Here are the helper classes I used above:以下是我在上面使用的辅助类:

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();
}

usually... you would have one main window and sub windows or user controls... let's suppose your MainWindow.xaml is the main window and you have a UserControl1 and UserControl2... and you want to switch between them... here you go: You can bind the "Content" property on the ContentPresenter to a property in your MainWindow.xaml.cs And that property will be modified in click handler by your buttons (or list …) Let's suppose you named that property "SelectedUserControl" Here is your MainWindow.xaml.cs: usually... you would have one main window and sub windows or user controls... let's suppose your MainWindow.xaml is the main window and you have a UserControl1 and UserControl2... and you want to switch between them... here您 go:您可以将 ContentPresenter 上的“Content”属性绑定到 MainWindow.xaml.cs 中的属性,并且该属性将在单击处理程序中通过您的按钮(或列表...)进行修改假设您将该属性命名为“SelectedUserControl”这是您的 MainWindow.xaml.cs:

public partial class MainWindow : Window,INotifyPropertyChanged
{
private UserControl1 myUserControl1= new UserControl1(){DataContext= new VM1()};
private UserControl2 myUserControl2= new UserControl2(){DataContext= new VM2()};
private UserControl _selectedUserControl;
public UserControl SelectedUserControl
{
get => _selectedUserControl;
set
{
_selectedUserControl = value;
OnPropertyChanged();
}
}
private void Button1_OnClick(object sender, RoutedEventArgs e)
{
SelectedUserControl = myUserControl1;
}
private void Button2_OnClick(object sender, RoutedEventArgs e)
{
SelectedUserControl = myUserControl2;
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

And for you MainWindow.xaml: you can modify it to:并且为您 MainWindow.xaml:您可以将其修改为:

<ContentPresenter Content="{Binding  SelectedUserControl,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=Window}}"></ContentPresenter>
<Button  Click="Button1_OnClick" >change to usercontrol 1</Button>
<Button  Click="Button2_OnClick" >change to usercontrol 2</Button>

You mustn't renew your viewmodel when selecting changed ….your viewModels must keep there values, you just switch userControls选择更改时,您不能更新您的视图模型......您的视图模型必须保留那里的值,您只需切换 userControls

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM