Архитектура в WPF MVVM + DI. Как передавать данные в дочерние вью модели?

Сразу вопрос, а потом описание. Как при резолве сервиса из контейнера передавать данные? Или с моей архитектурой изначально что-то не так?

У меня есть модель в БД, по структуре деревья. При запуске приложения нужно создать соответствующие ViewModel. Пока это работает так. В App вызывается асинхронный метод MainViewModel.InitAppData() для получения данных и создания вью моделей и экран загрузки сменяется готовой вьюшкой.

Класс App

protected override async void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);

    await AppHost!.StartAsync();

    var mainWindow = AppHost.Services.GetRequiredService<MainWindow>();
    var mainVM = AppHost.Services.GetRequiredService<MainViewModel>();
    mainWindow.DataContext = mainVM;
    mainWindow.Show();

    mainVM.InitAppData();
}

Класс MainViewModel

internal class MainViewModel : BaseViewModel
{
    readonly IServiceProvider? _serviceProvider;
    public MainViewModel(IServiceProvider? serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    bool _isDataLoaded = false;
    public async void InitAppData()
    {
        using var dbContext = _serviceProvider?.GetRequiredService<MainDbContext>();
        if (dbContext == null) return; //TODO: error handling
        var skills = await dbContext.GetTreesAsync();

        SkillTreeUC = new SkillTreeUC();
        _skillTreeVM = new SkillTreeViewModel(skills);
        SkillTreeUC.DataContext = _skillTreeVM;


        _isDataLoaded = true;
    }

    UserControl _skillTreeUC = new LoadingUC();
    public UserControl SkillTreeUC
    {
        get => _skillTreeUC;
        set
        {
            _skillTreeUC = value;
            RaisePropertyChanged(nameof(SkillTreeUC));
        }
    }
}

Класс SkillTreeViewModel

internal class SkillTreeViewModel : BaseViewModel
{
    /*Вот тут в конструктор передается модель, которая зависит от 
    предыдущей
    Тут мне нужно зарезолвить сервис и прокинуть в него эту модель
    И вот как добавить сервис в контейнер, оставив конструктору
    место для пользовательского свойства*/
    public SkillTreeViewModel(List<Skill> skills)//Тут корни деревьев
    {
        foreach(Skill skill in skills)
        {
            SkillVMs.Add(new(skill));
        }
    }
    public ObservableCollection<SkillViewModel> SkillVMs { get; set; } = new();
}

Класс SkillViewModel

internal class SkillViewModel : BaseViewModel
{
    public SkillViewModel(Skill skill)
    {
        Name = skill.Name;
        Description = skill.Description ?? String.Empty; ;
        Notes = skill.Notes ?? String.Empty;
        var chilldren = skill.Children ?? new();
        foreach (var child in chilldren)
        {
            SkillVMs.Add(new(child));
        }
    }
    public ObservableCollection<SkillViewModel> SkillVMs = new();
}

И все бы замечательно, но мне нужно использовать dbContext в дочерних моделях, и я понял что игнорирую существование DI контейнера. Но при первой инициализации мне нужно передавать в каждую дочернюю вью модель соответствующую модель.


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

Автор решения: Melkor V

В чем проблема хранить ссылку на ваш _serviceProvider? Не очень хорошо, но будет работать. Если хочется лучше - то передавайте ваш сервис MainDbContext, но сделайте в нем метод который будет вызывать Dispose не самого сервиса, а внутри метода, таким образом вам не придется думать о using для сервиса, да и это странно очень как по мне т.к. это класс для управления БД. Я бы сделал таким образом:

Создал класс у которого была бы зависимости от контекста базы данных либо какая-либо lazy ссылка на него. В этом классе были бы методы которые обращаются к базе данных и тут мы приходим к известному паттеру Репозиторий...

→ Ссылка
Автор решения: alex6327

Я лично использую так называемый VM-локатор.

 public class VM_Locator
    {
        public VM_Locator()
        {

        }

        public static IServiceScope scope;

        public VM_Window1 VM_Window1 => App.Host.Services.GetRequiredService<VM_Window1 >();
        public VM_Window2 VM_Window2 => App.Host.Services.GetRequiredService<VM_Window2 >();


        public static void Init()
        {
            scope?.Dispose();
            scope = App.Host.Services.CreateScope();
        }

        public VM_UserControl1 VM_UserControl1 => scope.ServiceProvider.GetRequiredService<VM_UserControl1 >();
        public VM_UserControl2 VM_UserControl2 => scope.ServiceProvider.GetRequiredService<VM_UserControl2 >();
        public VM_UserControl3 VM_UserControl3 => scope.ServiceProvider.GetRequiredService<VM_UserControl3 >();        
    }

Метод Init вызываю в OnStartup и при каждом открытии нового окна. Внутри любой VM могу обратиться к любому свойству или методу любой другой VM т.о.:

VM_Locator.scope.ServiceProvider.GetRequiredService<"нужная мне VM">()."нужное мне св-во";

Данный локатор внесен в ресурсы app.xaml

<Application.Resources>
        <vm:VM_Locator x:Key="Locator" />
    </Application.Resources>

И DataContext задаю в коде xaml вопреки распространённому здесь мнению, что так делать не нужно.

DataContext="{Binding "нужная мне VM", Source={StaticResource Locator}}"
→ Ссылка