c# WPF через время поменять контент в окне и передать данные

Задался вопросом как в коде через некоторое время менять контент в окне? Условно есть Main страница, я хочу по окончанию таймера который запущу в коде поменять на Additional страницу. Были идеи сделать это через ContentControl где я просто буду хранить текущий вид и по окончанию таймера менять на Additional, но мне нужно еще из Main части передать данные в Additional и тут встает второй вопрос могу ли я в MainVM хранить модели обеих страниц для взаимодействия или наоборот создать одну Model для обеих VM. Уверен оба способа не шибко хороши, поэтому хотел узнать тут есть ли какой-то более хороший метод для реализации этого?


Ответы (1 шт):

Автор решения: EvgeniyZ

Самое простое, и мне кажется, самое подходящее для вашего случая, это через конструктор просить нужные данные, или даже нужную 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>
    

Результат всего этого:

Result

Как видите, все передалось, все работает, контент поменялся, таймер тикает (гифка ускорена).

Какие проблемы?

  • У нас тесная связь 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, так что, если будете использовать эту библиотеку знайте, там все еще проще (писал пример тут).

Собственно, объяснил все, да и с заделом на будущее развитие.
Удачи в дальнейших изучениях!

→ Ссылка