C# WPF MVVM - навигация и сохранение состояния ViewModel

Я пытаюсь создать приложение WPF с 1 главным окном с 2 представлениями, между которыми можно перемещаться. Для этой реализации я использую набор инструментов Microsoft Community MVVM. Я следил за видео Джеймса Монтеманьо на YouTube и использовал класс обмена сообщениями MVVM Toolkit для реализации ChangeViewModelMessage. Проблема с этим подходом заключается в том, что он создает новую модель представления каждый раз, когда вы переключаетесь между представлениями.

Я хотел бы, чтобы состояния модели представления оставались неизменными при навигации по приложению. Я просмотрел несколько видеороликов, реализующих NavigationService, но ни в одном из них не использовался инструментарий Community MVVM Toolkit, и все они кажутся слишком сложными. Я думаю, мне нужно настроить одноэлементный экземпляр моделей представления, но я не уверен, как это сделать в WPF (Джеймс Монтеманьо использовал .NET Maui в своей демонстрации).

Я создал простую демонстрацию этой проблемы, состоящую из двух представлений, каждое из которых имеет метку, отображающую текущее представление, текстовое поле, в которое вы можете ввести текст (текст должен оставаться неизменным при переключении представлений), и кнопку, которая переключает вид.

Я разместил проект на GitHub, если вы хотите просмотреть его там или загрузить и внести изменения для себя. Проект можно найти здесь .

У меня есть еще один вопрос: как я могу динамически изменять минимальный размер главного окна, чтобы он соответствовал минимальному размеру каждого представления при переключении между представлениями?




App.xaml

      <Application
    x:Class="WpfApp1.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:v="clr-namespace:WpfApp1.Views">
    <Application.MainWindow>
        <v:MainWindow Visibility="Visible" />
    </Application.MainWindow>
    <Application.Resources />
</Application>



ВЗГЛЯДЫ


MainWindow.xaml

      <Window
    x:Class="WpfApp1.Views.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:v="clr-namespace:WpfApp1.Views"
    xmlns:vm="clr-namespace:WpfApp1.ViewModels"
    Title="MainWindow"
    Width="300"
    Height="300"
    Focusable="False"
    IsTabStop="False"
    WindowStartupLocation="CenterScreen"
    mc:Ignorable="d">
    <ContentControl
        Content="{Binding CurrentViewModel}"
        Focusable="False"
        IsTabStop="False">
        <ContentControl.Resources>
            <DataTemplate DataType="{x:Type vm:ViewModel1}">
                <v:View1 />
            </DataTemplate>
            <DataTemplate DataType="{x:Type vm:ViewModel2}">
                <v:View2 />
            </DataTemplate>
        </ContentControl.Resources>
    </ContentControl>
</Window>

MainWindow.xaml.cs

      using System.Windows;
using WpfApp1.ViewModels;

namespace WpfApp1.Views;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        // why do i have to set the data context here? 
        // it didn't work in MainWindow.xaml like it did for View1.xaml
        DataContext = new MainWindowViewModel(); 
    }
}

View1.xaml

      <UserControl
    x:Class="WpfApp1.Views.View1"
    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:vm="clr-namespace:WpfApp1.ViewModels"
    d:DataContext="{d:DesignInstance Type=vm:ViewModel1}"
    d:DesignHeight="200"
    d:DesignWidth="300"
    Background="LightGray"
    mc:Ignorable="d">
    <Grid>
        <StackPanel>
            <Label HorizontalAlignment="Center" Content="View 1" />
            <TextBox Text="{Binding TextBoxText}" />
            <Button Command="{Binding SwitchToView2Command}" Content="Switch to View 2" />
        </StackPanel>
    </Grid>
</UserControl>

View1.xaml.cs

      using System.Windows.Controls;

namespace WpfApp1.Views
{
    public partial class View1 : UserControl
    {
        public View1()
        {
            InitializeComponent();
        }
    }
}

View2.xaml

      <UserControl
    x:Class="WpfApp1.Views.View2"
    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:vm="clr-namespace:WpfApp1.ViewModels"
    d:DataContext="{d:DesignInstance Type=vm:ViewModel2}"
    d:DesignHeight="100"
    d:DesignWidth="200"
    Background="Gray"
    mc:Ignorable="d">
    <Grid>
        <StackPanel>
            <Label HorizontalAlignment="Center" Content="View 2" />
            <TextBox Text="{Binding TextBoxText}" />
            <Button Command="{Binding SwitchToView1Command}" Content="Switch to View 1" />
        </StackPanel>
    </Grid>
</UserControl>

View2.xaml.cs

      using System.Windows.Controls;

namespace WpfApp1.Views
{
    public partial class View2 : UserControl
    {
        public View2()
        {
            InitializeComponent();
        }
    }
}



ПОСМОТРЕТЬМОДЕЛИ


MainWindowViewModel.cs

      using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using WpfApp1.Messages;

namespace WpfApp1.ViewModels;

public partial class MainWindowViewModel : ObservableObject, IRecipient<ChangeViewModelMessage>
{

    [ObservableProperty]
    public ObservableObject currentViewModel;

    public MainWindowViewModel()
    {
        CurrentViewModel = new ViewModel1();
        WeakReferenceMessenger.Default.Register<ChangeViewModelMessage>(this);
    }

    public void Receive(ChangeViewModelMessage message)
    {
        CurrentViewModel = message.Value;
    }
}

ViewModel1.cs

      using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using WpfApp1.Messages;

namespace WpfApp1.ViewModels;

public partial class ViewModel1 : ObservableObject
{
    [ObservableProperty]
    private string textBoxText;

    public ViewModel1()
    {
        TextBoxText = "Enter text";
    }

    [RelayCommand]
    private void SwitchToView2()
    {
        WeakReferenceMessenger.Default.Send(
            new ChangeViewModelMessage(new ViewModel2()));
    }
}

ВидеоМодель2.cs

      using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using WpfApp1.Messages;

namespace WpfApp1.ViewModels;

public partial class ViewModel2 : ObservableObject
{
    [ObservableProperty]
    private string textBoxText;

    public ViewModel2()
    {
        TextBoxText = "Enter text";
    }

    [RelayCommand]
    private void SwitchToView1()
    {
        WeakReferenceMessenger.Default.Send(
            new ChangeViewModelMessage(new ViewModel1()));
    }
}



СООБЩЕНИЯ


Чанжевиевмоделмессаже.cs

      using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging.Messages;

namespace WpfApp1.Messages;

public class ChangeViewModelMessage : ValueChangedMessage<ObservableObject>
{
    public ChangeViewModelMessage(ObservableObject viewModel) : base(viewModel)
    {
    }
}

1 ответ

Основная проблема, которую я вижу, это вот эта строка:

WeakReferenceMessenger.Default.Send(new ChangeViewModelMessage(new ViewModel2()));

Каждый раз, когда вы нажимаете кнопку, вы создаете новыйViewModel2/1объект, поэтому вы фактически теряете значение, которое вы сохранили вtextBoxText. Вам нужно создать только 1 экземпляр и 1 дляViewModel2.

Вам нужно будет создать какой-то механизм для вашего приложения, чтобы управлять экземплярамиViewModelобъекты, которые вы создаете.

Есть много способов сделать это. Вот пример

      public partial class ViewModel1 : ObservableObject
{
    [ObservableProperty]
    private string textBoxText;

    private ObservableObject AlternativeVM;

    public ViewModel1()
    {
        TextBoxText = "Enter text";
    }

    public ViewModel1(ObservableObject altVM)
    {
       // store the ViewModel2 object we have received
       AlternativeVM = altVM;
    }

    [RelayCommand]
    private void SwitchToView2()
    {
        // if alt vm hasnt been created, do it now
        if (AlternativeVM == null)
            AlternativeVM = new ViewModel2(this);
        WeakReferenceMessenger.Default.Send(new ChangeViewModelMessage(AlternativeVM));
    }
}

я бы НЕ конвертировалViewModel1и2в синглтоны, потому что это сковывает вас в будущем, когда вы, возможно, захотите повторно использовать эту ViewModel, а повторное использование является одним из основных направлений MVVM.

Другие вопросы по тегам