7

I am new to Avalonia/ WPF, Xaml and desktop development in general so please forgive and clarify any related misunderstandings I demonstrate. I will continue to study available documentation but I am having a difficult time finding material which addresses the point I am getting stuck on.

I am trying to implement a composition-root, constructor-injection based dependency-injection system in my Avalonia application, using the recommended MVVM pattern and associated Avalonia project template. I have some familiarity with the Microsoft.Extensions.DependencyInjection package so have been trying to work with this system.

Between tutorials for WPF and Avalonia based on this DI framework as well as other frameworks, I have tried to piece together a working solution. I think I have things figured out conceptually as far as registering Services and ViewModels and setting up constructors for these classes appropriately such that the framework will inject dependencies into these classes on instantiation. However, where I am getting stuck is with how to implement constructor injection for View classes.

I attempted to register both MainWindow and MainWindowViewModel as services:

// App.axaml.cs
public partial class App : Application
    {
        private IServiceProvider _services;
        
        public override void Initialize()
        {
            AvaloniaXamlLoader.Load(this);
        }

        
        public override void OnFrameworkInitializationCompleted()
        {
            ConfigureServiceProvider();

            if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
            {
                desktop.MainWindow = _services.GetService<MainWindow>();
            }
            
            base.OnFrameworkInitializationCompleted();
        }
        
        
        private void ConfigureServiceProvider()
        {
            var services = ConfigureServices();
            _services = services.BuildServiceProvider();
        }
        
        private static IServiceCollection ConfigureServices()
        {
            var services = new ServiceCollection();
            
            services.AddTransient<MainWindow>();
            services.AddTransient<MainWindowViewModel>();

            return services;
        }
    }

The goal is then to be able to inject the MainWindowViewModel class into the MainWindow class via constructor and then assign that argument to the DataContext property of the MainWindow view-class:

// MainWindow.axaml.cs
public partial class MainWindow : Window
    {
        public MainWindow(MainWindowViewModel viewModel)
        {
            DataContext = viewModel;
            InitializeComponent();
#if DEBUG
            this.AttachDevTools();
#endif
        }

        private void InitializeComponent()
        {
            AvaloniaXamlLoader.Load(this);
        }
    }

However, this causes the following error to be raised:

  MainWindow.axaml(1, 2): [XAMLIL] Unable to find public constructor for type MyApp.Client:MyApp.Client.Views.MainWindow() Line 1, position 2.

It seems the View cannot be instantiated without the existence of a parameter-less constructor, however, this would seem to prevent constructor injection.

It is very possible I have some fundamental misunderstanding about the intended relationship between ViewModels and Views. I have come across a number of examples where ViewModels are not registered with the service-container, and instead are instantiated directly in the View constructor and assigned to the DataContext property. I would prefer to avoid this approach.

Meanwhile, every tutorial I have come across which demonstrates injecting ViewModels into corresponding View classes, does so using the Service Locator pattern, where the DI service container is passed explicitly (or invoked as a global object) and the ViewModel is resolved explicitly from the container.

Can anybody direct me to any example source code or tutorial which demonstrates how to properly inject ViewModels into Views via constructor? Is this possible to achieve? Is there something I can modify in the MainWindow.axaml file to enable the desired behavior? Thank you for your time and again, I would greatly appreciate clarification of any misunderstandings I may have.

Just for reference, here is the MainWindow markup:

// MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:MyApp.Client.ViewModels"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
        x:Class="MyApp.Client.Views.MainWindow"
        x:DataType="vm:MainWindowViewModel"
        x:CompileBindings="True"
        Icon="/Assets/avalonia-logo.ico"
        Title="MyApp">

    <TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/>

</Window>
tyhdev
  • 123
  • 1
  • 3
  • 7

1 Answers1

5

View models are associated with views via DataContext instead of constructor injection. Note that a singe view can be reusable (especially if you are dealing with virtualized lists).

In general your DI should not know about most of the view part at all, it should be only concerned with ViewModel and lower layers.

Instead of being created via DI views are usually located via view locator by other views that bind particular properties to ContentControl, e. g.

<ContentControl Content="{Binding MySubViewModel} />

(you can find a simple view locator in avalonia.mvvm template, you can tune it for your needs). When one needs to show a new top-level view from your view model code, they usually implement some kind of a window manager that manages top-level views and is accessible from the view model via DI, e. g.

    public class ViewManager : IViewManager
    {
        private Window CreateWindowForModel(object model)
        {
            foreach (var template in Application.Current.DataTemplates)
            {
                if (template.Match(model))
                {
                    var control = template.Build(model);
                    if (control is Window w)
                        return w;
                    return new Window { Content = control };
                }
            }

            throw new KeyNotFoundException("Unable to find view for model: " + model);
        }

        public void ShowWindow(object model) => CreateWindowForModel(model).Show();
    }

Then you add IViewManager implementation to your DI.

Note that this approach is reusable for all XAML frameworks and makes it possible to completely reuse the view model between various platforms (e. g. if you want to implement mobile UI with Xamarin and desktop with Avalonia) with only a few UI-toolkit specific services.

kekekeks
  • 3,193
  • 19
  • 16