c# WPF через время поменять контент в окне и передать данные
Задался вопросом как в коде через некоторое время менять контент в окне? Условно есть Main
страница, я хочу по окончанию таймера который запущу в коде поменять на Additional
страницу. Были идеи сделать это через ContentControl
где я просто буду хранить текущий вид и по окончанию таймера менять на Additional
, но мне нужно еще из Main
части передать данные в Additional
и тут встает второй вопрос могу ли я в MainVM
хранить модели обеих страниц для взаимодействия или наоборот создать одну Model
для обеих VM
. Уверен оба способа не шибко хороши, поэтому хотел узнать тут есть ли какой-то более хороший метод для реализации этого?
Ответы (1 шт):
Самое простое, и мне кажется, самое подходящее для вашего случая, это через конструктор просить нужные данные, или даже нужную VM в другой VM. Тут главное не путать слои.
Простой пример
ViewModel слои
Главная VM:
public partial class MainViewModel : ObservableObject { [ObservableProperty] private object _currentPage; public MainViewModel() { CurrentPage = new HomePageViewModel(this); } }
VM главной страницы окна:
public partial class HomePageViewModel(MainViewModel mainViewModel) : ObservableObject { private readonly MainViewModel _mainViewModel = mainViewModel; [ObservableProperty] private TimeSpan _time; [RelayCommand] private async Task Run() { Time = TimeSpan.FromSeconds(5); var delay = TimeSpan.FromSeconds(1); while (Time > TimeSpan.Zero) { await Task.Delay(delay); Time -= delay; } _mainViewModel.CurrentPage = new ResultPageViewModel(Random.Shared.Next(1, 1000)); } }
VM страницы результатов:
public class ResultPageViewModel(int score) { public int Score { get; } = score; }
Пояснения:
В
MainViewModel
как видите есть свойство, которое содержит текущую отображаемую страницу (CurrentPage
), нам его надо заменить за пределами данной VM. Как быть? Как и сказал выше, передача через конструктор ссылки на эту главную VM куда надо (в моем случаеnew HomePageViewModel(this);
).В
HomePageViewModel
мы принимаем эту главную VM (HomePageViewModel(MainViewModel mainViewModel)
) и сохраняем ссылку для дальнейшего использования. Вот и вся передача.Далее, вы просили таймер, в
HomePageViewModel
как видите есть асинхронная задачаRun
, которая будет вызываться по клику кнопки, в ней мы просто крутим бесконечный цикл до тех пор, пока таймер не будет равен0
, ну и каждую итерацию меняем время и ждем заданное время (1 сек.).По окончанию импровизированного "таймера", мы главной VM меняем контент на
ResultPageViewModel
, которой я для примера передаю еще и случайное число (условно, счет игрока). И вот заметьте, опять мы через конструктор передаем, но уже просто значение.Ну а
ResultPageViewModel
максимально прост, мы просто забираем входящее значение и заносим его в свойство для привязки.
XAML
Окно:
<Window.Resources> <DataTemplate DataType="{x:Type local:HomePageViewModel}"> <local:HomePage /> </DataTemplate> <DataTemplate DataType="{x:Type local:ResultPageViewModel}"> <local:ResultPage /> </DataTemplate> </Window.Resources> <ContentPresenter Content="{Binding CurrentPage}" />
HomePage
:<StackPanel VerticalAlignment="Center" > <TextBlock HorizontalAlignment="Center" FontSize="30" FontWeight="Medium" Text="{Binding Time}" /> <Button HorizontalAlignment="Center" Command="{Binding RunCommand}" Content="Старт" /> </StackPanel>
ResultPage
:<Viewbox> <StackPanel> <TextBlock Margin="10,0,10,-5" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="16" FontWeight="Medium" Text="Результат" Typography.Capitals="AllSmallCaps" /> <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="15" Text="{Binding Score}" /> </StackPanel> </Viewbox>
Результат всего этого:
Как видите, все передалось, все работает, контент поменялся, таймер тикает (гифка ускорена).
Какие проблемы?
У нас тесная связь VM слоев, а это в свою очередь плодит проблемы по типу:
Трудности в поддержке и расширении - Если компоненты тесно связаны, изменение в одном компоненте может вызвать неожиданные последствия в других компонентах.
Низкая переиспользуемость - Компоненты, которые тесно связаны с другими компонентами, обычно трудно переиспользовать в других контекстах или проектах, поскольку они зависят от своих связей.
Трудности в тестировании - Тесно связанные компоненты обычно труднее тестировать изолированно.
Как быть?
Тут все зависит от проекта и требований. Для небольших проектов такая связанность особых проблем не принесет. А вот если проект большой, то стоит задуматься... Давайте попробуем решить эти проблемы:
Основное проблемное место тут, это изменение страницы в главной VM, и если взглянуть на код, то мы понимаем, что тут нужен некий механизм оповещения, который будет независим, который позволит в одном месте оповестить, а в другом подписаться на эти оповещения. Давайте сделаем это:
Пусть будет класс, который будет хранить тип оповещения (это может быть что угодно, хоть отдельные
EventArgs
(думаю встречались), а такжеAction
(действие), которое должно произойти при оповещении данного типа, получаем примитивную "шину событий":public class EventBus { private readonly Dictionary<Type, List<Action<object>>> _subscriptions = []; public void Subscribe<T>(Action<T> action) { var type = typeof(T); if (!_subscriptions.TryGetValue(type, out List<Action<object>>? value)) { value = []; _subscriptions[type] = value; } value.Add(x => action((T)x)); } public void Publish<T>(T data) { if (data is null) return; var type = typeof(T); if (_subscriptions.TryGetValue(type, out List<Action<object>>? value)) { foreach (var action in value) { action?.Invoke(data); } } } }
Переписываем теперь главную VM
public MainViewModel() { var eventBus = new EventBus(); eventBus.Subscribe<int>(score => CurrentPage = new ResultPageViewModel(score)); CurrentPage = new HomePageViewModel(eventBus); }
Тут мы создаем "шину", подписываемся на нужный тип события, и прописываем действие, которое должно произойти (смена VM).
В
HomePageViewModel
меняем зависимость отMainViewModel
наEventBus
, а смену VM меняем на простое_eventBus.Publish(Random.Shared.Next(1, 1000));
Запускаем, проверяем.
Как видите, VM слои теперь имеют "слабую зависимость" от EventBus
, слои не связаны друг с другом, а имея эту шину, мы можем теперь где угодно подписаться на какое угодно событие. У вас может возникнуть вопрос "но ведь у нас есть new ResultPageViewModel(score);
, что делает MainViewModel
зависимой от ResultPageViewModel
- да, это так, но, это не обязательно проблема, если MainViewModel
предназначена для управления переходами между различными ViewModel
. В данном примере, MainViewModel
может быть рассмотрена как координатор или навигационный менеджер. Но мы можем и от этой зависимости избавиться, сделав "фабрику", к примеру, делаем интерфейс:
public interface IViewModelFactory
{
ResultPageViewModel CreateResultPageViewModel(int score);
// Другие методы для создания других ViewModel...
}
Внедряем его в MainViewModel
, и все, она больше не создает напрямую другие VM. Но опять, все зависит от вашего приложения, ваших требований.
Кстати, "шина событий" есть в библиотеке CommunityToolkit.MVVM
, которая используется в этом примере для генерации INotifyPropertyChanged
и ICommand
, так что, если будете использовать эту библиотеку знайте, там все еще проще (писал пример тут).
Собственно, объяснил все, да и с заделом на будущее развитие.
Удачи в дальнейших изучениях!