Архитектура в 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 шт):
В чем проблема хранить ссылку на ваш _serviceProvider? Не очень хорошо, но будет работать. Если хочется лучше - то передавайте ваш сервис MainDbContext, но сделайте в нем метод который будет вызывать Dispose не самого сервиса, а внутри метода, таким образом вам не придется думать о using для сервиса, да и это странно очень как по мне т.к. это класс для управления БД. Я бы сделал таким образом:
Создал класс у которого была бы зависимости от контекста базы данных либо какая-либо lazy ссылка на него. В этом классе были бы методы которые обращаются к базе данных и тут мы приходим к известному паттеру Репозиторий...
Я лично использую так называемый 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}}"