Открытие окна из vm окна через serviceProvider или свой велосипед
Есть пока две мысли по этому поводу:
- Имеется такая конфигурация дефолтного DI:
services.AddTransient<MainWindow>(); services.AddTransient<MainViewModel>();
получаю окно и открываю:
var view = _serviceProvider.GetService<MainWindow>();
view.Show();
В каждую зависимость прокидывается автоматически ServiceProvider если я попрошу его в конструкторе. И через него я точно также могу просить окно и открывать. Можно ли так через serviceprovider открывать окно из vm окна если требуется еще одно.
Или же 2. Придумать велосипед в виде отдельного оконного сервиса в котором запрашивается serviceprovider и, который будет сопоставлять в словаре vm и окно.
Пока писал появилась третья мысль сделать статический класс и метод, которые будут отвечать за получение окна и vm для него, просто в vm классах запрашивать у этого метода, нужное окно и открывать. Может ли появиться проблема с внедрением зависимостей туда?
Может быть есть уже готовое решение по этому поводу?
Спасибо за внимание)
Ответы (1 шт):
В MVVM подходе ваш код должен быть максимально разделен на слои, которые должны быть мало связаны друг с другом. Самое важное в MVVM - это отделить UI от логики, то есть View от ViewModel и Model. Исходя из этого, вы не можете в VM слое напрямую вызвать что-то, что относиться к View слою. Создать окно, диалог, да даже MessageBox.Show() (MessageBox - это UI (View слой)) - это будет нарушением в MVVM. Все это, вы должны максимально абстрагировать и отделить от основной логики, отдав задачу на открытие окон некому, сервису.
Покажу простой пример
Создадим интерфейс, который опишет все нужные методы сервиса. В моем примере я покажу открытие окна и открытие диалогового окна (модальное).
public interface IWindowService { void Show<TViewModel>(); bool? ShowDialog<TViewModel>(); }Теперь реализуем сам сервис. Его реализовать можно по разному, основные подходы это:
- Следование некому соглашению имен, где скажем
MainWindowбудет иметь VM с именемMainViewModel, аTestWindow-TestViewModel, и т.д. То есть,Windowменяем наViewModel. - Сделать свой "контейнер", который будет хранить в себе все сопоставления VM с их View, а при запуске приложения регистрировать туда все типы. (этот пример я показывал в комментариях).
Я чуть заморочусь и пойду первым путем. Правда для примера он немного сложноват, но вы главное поймите принцип, а реализацию напишите какую хотите.
public class WindowService(IServiceProvider provider) : IWindowService { public void Show<TViewModel>() { var viewModel = provider.GetRequiredService<TViewModel>(); var window = CreateWindowFor<TViewModel>(); window.DataContext = viewModel; window.Show(); } public bool? ShowDialog<TViewModel>() { var viewModel = provider.GetRequiredService<TViewModel>(); var window = CreateWindowFor<TViewModel>(); window.DataContext = viewModel; return window.ShowDialog(); } private Window CreateWindowFor<TViewModel>() { var vmName = typeof(TViewModel).Name; var windowTypeName = vmName.EndsWith("ViewModel") ? vmName.Replace("ViewModel", "Window") : vmName + "Window"; var windowType = AppDomain.CurrentDomain .GetAssemblies() .SelectMany(a => a.GetTypes()) .FirstOrDefault(t => t.Name == windowTypeName && typeof(Window).IsAssignableFrom(t)); return provider.GetRequiredService(windowType) as Window; } }Как видите, тут не так все сложно. Создаем класс, наследуем его от интерфейса и реализуем все методы. Каждый метод принимает тип VM, который контейнер нам создает. В методе
CreateWindowForберется имя типа (MainViewModel), и меняетсяViewModelнаWindow(получаемMainWindow), далее при помощи рефлексии находит тип окна по полученному имени, ну и отдаем его контейнеру, чтобы тот создал. P.S. Отлов ошибок специально не делал, ибо хотел максимально просто показать суть. Так что, сами допишите то, что надо.- Следование некому соглашению имен, где скажем
Остается дело за малым, создаем окна, делаем для них свои VM, ну и регистрируем в контейнер. У меня будет
MainWindowкак стартовое окно, а такжеTestWindowв качестве подопытного. Весь классAppс регистрациями и открытием главного окна будет примерно следующим:public partial class App : Application { private readonly ServiceProvider _serviceProvider; public App() { var serviceCollection = new ServiceCollection(); ConfigureServices(serviceCollection); _serviceProvider = serviceCollection.BuildServiceProvider(); } private void ConfigureServices(IServiceCollection services) { services.AddSingleton<IWindowService, WindowService>(); services.AddTransient<MainWindow>(); services.AddTransient<TestWindow>(); services.AddTransient<MainViewModel>(); services.AddTransient<TestViewModel>(); } protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); var windowService = _serviceProvider.GetRequiredService<IWindowService>(); windowService.Show<MainViewModel>(); } protected override void OnExit(ExitEventArgs e) { base.OnExit(e); _serviceProvider?.Dispose(); } }На этом все. Теперь в любом месте вашего приложения вы можете запросить
IWindowServiceи вызвать нужный метод, указав конкретную VM нужного окна. По аналогии можно подобным способом "обернуть" любой вызов и добиться любой реализации, хоть запрашивайте данные и возвращайте их обратно, ну или двигайте/сворачивайте/изменяйте окна. Тут уже ваша фантазия.
На последок приведу пример того, что это работает:
Класс MainViewModel:
internal partial class MainViewModel(IWindowService windowService)
{
public string Text { get; } = "Привет из MainViewModel!";
[RelayCommand]
void Open()
{
windowService.Show<TestViewModel>();
}
[RelayCommand]
void OpenModal()
{
windowService.ShowDialog<TestViewModel>();
}
}
В TestViewModel аналогично, но со своим текстом и без команд. Команды если что реализованы при помощи CommunityToolkit.MVVM.
XAML MainWindow:
<Grid>
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock
Grid.ColumnSpan="2"
Margin="5"
VerticalAlignment="Center"
FontSize="20"
FontWeight="Medium"
Text="{Binding Text}" />
<Button
Grid.Row="1"
Grid.Column="0"
Margin="2,0"
HorizontalAlignment="Stretch"
Command="{Binding OpenCommand}"
Content="Открыть" />
<Button
Grid.Row="1"
Grid.Column="1"
Margin="2,0"
HorizontalAlignment="Stretch"
Command="{Binding OpenModalCommand}"
Content="Открыть модально" />
</Grid>
</Grid>
Результат (не модальные окна):
Результат (модальное, основное окно, как и положено, заблокировано)

