1

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);
    }
}

2 Answers 2

0

Some considerations to improve your code:

  • GetService<T> should not be public static but private or private static. Declaring the method as public opens the door for referencing the container from your application code. If you need to create instances dynamically use a factory (e.g. Abstract Factory pattern).

  • Don't use an Interaction trigger only to react on selected item changes. there is no need to let the mouse event trigger a command. It would be much straighter to handle the property change in the view model. Because the SelectedContainer property is already changing when ListBox.SelectedItem changes you don't need an additional and therefore redundant notification. See the below example.

  • You full property declarations are wrong. You are currently defining a private auto property and reference it from the public full property like this:

    // Wrong: this is an auto property to backup the full property. 
    // Instead, the backup property must be a field.
    private Container _selectedContainer { get; set; }
    
    // Wrong: this property is backed by a property instead of a field
    public Container SelectedContainer { get => _selectedContainer; } set => _selectedContainer = value; } 
    

    But you must use backing fields instead:

    // Correct: the backing FIELD
    private Container _selectedContainer;
    
    // Correct: the full property that uses the backing FIELD
    public Container SelectedContainer { get => _selectedContainer; } set => _selectedContainer = value; } 
    
  • Also, your class and symbol names are misleading as container is a term used in context of UI. In the view, a container wraps the data item e.g., ListBoxItem is a item container. Just in case: if you are handling container controls in your view model, then this is wrong and can and should be avoided. It's not clear form your question what Container is.

  • The standard C# naming convention requires field names to be camelCase and not PascalCase e.g. private int _numericValue;. Your naming style looks strange.

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>
Sign up to request clarification or add additional context in comments.

6 Comments

Thanks very much. I express my great gratitude to you.
Thanks for your appreciation. I'm happy if I could help.
If it doesn't bother you, please tell me this: code ContainerContentViewModel content = _containerContentViewModelFactory.Invoke(); code returns this error: System.InvalidOperationException: "No service for type 'Calculcation.App.UI.Controls.ContainerContentViewModel' has been registered." Where could I have made a mistake?
You are welcome. The exception is thrown is because the type that the factory delegate returns was not registered with the IoC container. I missed this line in my example as I was only focused to show how to register the delegate. I missed to show that you must also register the dependencies of this delegate. i have updated th answer to show that you must also register the the Func<ContainerContentViewModel> dependencies which is the ContainerContentViewModel.
I'm sorry to bother you, I'm an inexperienced programmer, but I was able to create a working code based on your code, and I'm very very grateful to you. Now, after what I have done here, I would like to know your professional opinion? isn't my implementation a bad? Below I will add the answer with the code, please take a look.
|
0

MainView.cs

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

MainViewModel.cs

public class MainViewModel : ViewModelBase, IMainViewModel
{
    private readonly Func<int,ContainerContentViewModel> _containerContentViewModelFactory;

    private readonly IRepositories _repositories;
    private readonly IAbstractFactory<ChangeExecutorView> _factory;

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

            OnSelectedContainerChanged();
        }
    }

    private object _theContent;
    public object TheContent
    {
        get => _theContent;
        set {
            _theContent = value;
            OnPropertyChanged(nameof(TheContent));
        }
    }
    // Declare the factory as constructor dependency
    public MainViewModel(IRepositories repositories, Func<int, ContainerContentViewModel> containerContentViewModelFactory) {
        _repositories = repositories;
        _containerContentViewModelFactory = containerContentViewModelFactory;

        ContainerList = new ObservableCollection<Container>(_repositories.ContainerRepository.GetAll());
    }
    // Here is action for create new UserControl
    public void OnSelectedContainerChanged() {
        ContainerContentViewModel content = _containerContentViewModelFactory.Invoke(SelectedContainer.Code);
        // The property will update the ContentControl
        // which is using a DataTemplate to load the actual control
        TheContent = content;
    }

    public MainViewModel(IRepositories repositories, IAbstractFactory<ChangeExecutorView> factory)
    {
        _repositories = repositories;
        _factory = factory;
        ContainerList = new ObservableCollection<Container>(_repositories.ContainerRepository.GetAll());
    }
}

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

ContainerContent.cs and ContainerContentViewModel.cs

public partial class ContainerContentView : UserControl
    {
        public ContainerContentView()
        {
            //this.DataContext = viewModel;
            InitializeComponent();
        }
    }
    public class ContainerContentViewModel : IContainerContentViewModel
    {
        IRepositories _repositories { get; }
        public ObservableCollection<object> ContentList { get; set; }
        //Without this constructor, an error occurs when initializing AppHost = Host.CreateDefaultBuilder()
        //InvalidOperationException: Unable to resolve service for type 'System.Int32' 
        //while attempting to activate 'App.UI.Controls.ContainerContentViewModel'. 
        public ContainerContentViewModel(IRepositories repositories)
        {
            this._repositories = repositories;
            ContentList = new ObservableCollection<object>(
                _repositories.ContentRepository.GetContentByContainerBocode(0)); ;
        }
        public ContainerContentViewModel(int pContainerBocode,IRepositories repositories) {
            this._repositories = repositories;
            ContentList = new ObservableCollection<object>(
                _repositories.ContentRepository.GetContentByContainerBocode(pContainerBocode)); ;
        }

        private ObservableCollection<object> CreateContentList(IEnumerable<object> listItems)
        {
            return null;
        }
    }

    public interface IContainerContentViewModel
    {

    }

App : Application

public partial class App : Application
    {
        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) => 
                {
                /*Working code but*/
                services.AddSingleton<Repositories>();//<--- it's wrong? 
                //It was done only in order to transfer the necessary dependency
                //to the Container Content View Model, is this correct ????
                    services.AddSingleton<ContainerContentViewModel>();
                    services.AddSingleton<Func<int, ContainerContentViewModel>>(
                      serviceProvider => (pContainerBocode) 
                      => new ContainerContentViewModel(pContainerBocode, serviceProvider.GetRequiredService<Repositories>()));
                    services.AddSingleton<MainView>();
                    services.AddTransient<IMainViewModel, MainViewModel>();
                    services.AddTransient<IRepositories,Repositories>();
                    //services.AddFormFactory<ChangeExecutorView>();
                    //services.AddScoped<ContainerContentViewModel>();

                    RepositoryServiceCollectionExtention.RegisterServices(services);
                                       
                })
                .Build();
        }

        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);
        }
    }
}

4 Comments

So far so good. Som remarks: MainViewModel defines two constructors that don't fully initialize the instance. ContainerContentViewModel and MainViewModel must both implement INotifyPropertyChanged, even when they don't contain changing properties. The moment a class serves as a binding source and is not a DependencyObject and can therefore not implement properties as dependency properties, a binding source must implement INotifyPropertyChanged. Otherwise, you are creating a memory leak.
ContainerContentViewModelthe properties that expose ObservanleCollections should be read-only. To modify them use Clear and Add. Replacing the complete collection instance is expensive for the UI. Reuse ObservableCollection binding sources. In App you should in addition to OnExit also handle unhandled exceptions by handling the Application.DispatcherUnhandledException and AppDomain.UnhandledException event if you have to perform important cleanup. OnExit will not get called in the event of an exception.
You shouldn't post this as an answer. I think it would be better to edit your question and add additional content there.
IMainViewModel should also implement INotifyPropertyChanged to make the API available via the IMainViewModel interface. Otherwise you would always have to cast to the concrete implementation MainViewModel which would defy the whole dependency injection (or IoC).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.