wpfdependency-injectioninversion-of-controlcode-injection

Dependency Injection with parameters


I have a WPF application. In the main window on the left is a listbox with several entries, and on the right is a ContentControl into which, when selecting one of the entries, the UserControl along with the view model should be loaded.

Next, when selecting one of the entries in the listbox, a UserControl instance with a view model should be created, the selected element from the listbox or one of its fields should be passed to the view model constructor.

I do not know how to do this correctly without creating a new instance of the UserControl and view model manually, without violating the principles of DI, if you create an instance manually, then the application is not cleared from memory when closed.

MainView.Xaml:

<ListBox ItemsSource="{Binding ContainerList}" SelectedItem="{Binding SelectedContainer}" 
                 HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="MouseLeftButtonUp">
                    <i:InvokeCommandAction Command="{Binding ShowContent}"/>
                </i:EventTrigger>
            </i:Interaction.Triggers>
</ListBox>
<ContentControl Grid.Column="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="{Binding TheContent}"></ContentControl>

Code behind:

public partial class MainView : Window
{
    public MainView(IMainViewModel viewModel)
    {
        this.DataContext = viewModel;
        InitializeComponent();
    }
}

public class MainViewModel : ViewModelBase, IMainViewModel
{
    private readonly IRepositories _repositories;
    private readonly IAbstractFactory<ChangeExecutorView> _factory;

    public ObservableCollection<Container> ContainerList { get; set; }
    private Container _SelectedContainer { get; set; }
    public Container SelectedContainer { get { return _SelectedContainer; } set { _SelectedContainer = value;  OnPropertyChanged(nameof(SelectedContainer)); } 
    }

    private object _TheContent { get; set; }
    public object TheContent
    {
        get { return _TheContent; }
        set {_TheContent = value; OnPropertyChanged(nameof(TheContent)); }
    }
    
    // public MainViewModel(IContainerRepository repContainer, IAbstractFactory<ChangeExecutorView> factory)
    public MainViewModel(IRepositories repositories, IAbstractFactory<ChangeExecutorView> factory)
    {
        _repositories = repositories;
        _factory = factory;
        ContainerList = new ObservableCollection<Container>(_repositories.ContainerRepository.GetAll());
    }

    // Here is action for create new UserControl
    public ICommand ShowContent 
    {
        get {
            return new RelayCommand(delegate (object param) 
                       {
                           // var content = new ContainerContentView(
                           // new ContainerContentViewModel(_repositories, SelectedUserID));
                       });
        }
    }
}

public interface IMainViewModel
{
    ObservableCollection<Container> ContainerList { get; set; }
}

app.xaml:

public static IHost AppHost { get; set; }

public App()
{
    AppHost = Host.CreateDefaultBuilder()
                .ConfigureHostConfiguration((hostConfiguration => {
                    hostConfiguration.AddJsonFile("appsettings.json",false,true)
                    .AddEncryptedProvider()
                    .AddJsonFile($"appsettings.json", false, true);

                }))
                .ConfigureServices((hostConext, services) => 
                {
                    services.AddSingleton<MainView>();
                    services.AddTransient<IMainViewModel, MainViewModel>();
                    services.AddTransient<IRepositories,Repositories>();
                    services.AddFormFactory<ChangeExecutorView>();
                    services.AddScoped<ContainerContentViewModel>();
                })
                .Build();
    }

    public static T GetService<T>() where T : class 
    {
        var service = AppHost.Services.GetService(typeof(T)) as T;
        return service;
    }

    protected override async void OnStartup(StartupEventArgs e)
    {
        await AppHost.StartAsync();//.ConfigureAwait(true);

        var startupForm = AppHost.Services.GetRequiredService<MainView>();
        startupForm.Show();
        base.OnStartup(e);
    }

    protected override async void OnExit(ExitEventArgs e)
    {
        await AppHost.StopAsync();

        base.OnExit(e);
    }
}

Solution

  • Some considerations to improve your code:

    To answer your question:

    In general, you don't create controls in C# code. Instead, you would dynamically create a data model using a factory and use a DataTemplate to let the framework create the control for you - one for each instance of the data model.

    1. First register a factory service that will be imported by the MainViewModel. You can create a custom abstract factory for more complex creation (e.g. when the factory method contains many parameters or the created instance requires additional configuration) or register a simple Func<T> (parameterless or with parameters):
    ServiceProvider container = new ServiceCollection()
      .AddSingleton<ContainerContentViewModel>()
      .AddSingleton<Func<ContainerContentViewModel>>(serviceProvider => serviceProvider.GetRequiredService<ContainerContentViewModel>)
      .AddSingleton<IMainViewModel, MainViewModel>()
      .BuildServiceProvider(new ServiceProviderOptions() { ValidateOnBuild = true });
    
      // More examples to show how to register factories 
      // for more complex instance creation:
      //
      // If you need to register a factory that requires dynamic parameters 
      // (provided by the caller of the factory):
      .AddSingleton<Func<string, int, ContainerContentViewModel>>(
        serviceProvider => (name, number) => new ContainerContentViewModel(name, number))
    
      // If you need to configure the instance created by the factory 
      // with dynamic parameters (provided by the caller of the factory):
      .AddSingleton<Func<string, int, ContainerContentViewModel>>(serviceProvider => (name, number) 
        => 
        {
          var viewModel = serviceProvider.GetRequiredService<ContainerContentViewModel>();
          viewModel.Name = name;
          viewModel.Number = number;
       
          return viewModel;
        }
      
    
    1. Inject the factory into the MainViewModel using the constructor and use it to dynamically create instances:

    MainViewModel.cs

    public class MainViewModel : ViewModelBase, IMainViewModel
    {
      private readonly Func<ContainerContentViewModel> _containerContentViewModelFactory;
    
      private Container _selectedContainer;
      public Container SelectedContainer
      {
        get => _selectedContainer;
        set 
        { 
          _selectedContainer = value; 
          OnPropertyChanged(nameof(SelectedContainer)); 
    
          // Load the new view content by assigning a data model to 
          // the TheContent property. 
          // Use this variant over an InteractionBehavior and a ICommand 
          // (which were introduced only to react to property changes).
          OnSelectedContainerChanged();
        }
      }
    
      private object _theContent;
      public object TheContent
      {
        get => _theContent;
        set 
        { 
          _theContent = value; 
          OnPropertyChanged(nameof(TheContent)); 
        }
      }
    
      // Declare the factory as constructor dependency
      public MainViewModel(Func<ContainerContentViewModel> containerContentViewModelFactory)
      {
        _containerContentViewModelFactory = containerContentViewModelFactory;
      }
    
      // Here is action for create new UserControl
      public void OnSelectedContainerChanged()
      {
        ContainerContentViewModel content = _containerContentViewModelFactory.Invoke();
    
        // The property will update the ContentControl
        // which is using a DataTemplate to load the actual control
        TheContent = content;
      }
    }
    

    MainView.xaml

    <Window>
      <Window.Resources>
    
        <!-- 
             The implicit DataTemplate that is automatically loaded 
             by the ContentControl 
        -->
        <DataTemplate DataType="{x:Type ContainerContentViewModel}">
          <ContainerContentView />
        </DataTemplate>
      </Window.Resources>
    
      <Grid>
        <ListBox ItemsSource="{Binding ContainerList}" 
                 SelectedItem="{Binding SelectedContainer}" />
    
        <ContentControl Content="{Binding TheContent}" />
      </Grid>
    </Window>