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.