Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add readme "How To Implement Dependency Injection with Pure.DI code generator" #541

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
---
id: how-to-implement-dependency-injection-with-pure-di
title: How To Implement Dependency Injection with Pure.DI code generator
---

# How To Implement Dependency Injection in Avalonia with Pure.DI code generator

This example is based on [another example](how-to-implement-dependency-injection.md) on dependency injection showing the benefits of the pure Dependency Injection approach using the Pure.DI source code generator.

## Step 0: Presentation model

Compared to [this example](how-to-implement-dependency-injection.md), only a few properties and methods have been added to demonstrate real-world usage scenarios:

```csharp
public interface IMainViewModel
{
string Title { get; }

string Greetings { get; }
}

public class MainViewModel(IBusinessService businessService)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Primary constructors are a fairly new feature. They should probably be avoided for now to ensure anybody copying and pasting the code can use it without worrying about their language version.

: IMainViewModel
{
public string Title => "Avalonia application";

public string Greetings => businessService.CreateGreetings();
}

public interface IBusinessService
{
string CreateGreetings();
}

public class BusinessService(IRepository repository)
: IBusinessService
{
public string CreateGreetings()
{
repository.RegisterSomething();
return "Example of Dependency Injection implementation using Pure.DI";
}
}

public interface IRepository
{
void RegisterSomething();
}

public class Repository: IRepository
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix the formatting on this line

{
public void RegisterSomething()
{
}
}
```

## Step 1: Install the NuGet package for DI

Run the following command in a terminal inside your project directory to install the [Pure.DI](https://www.nuget.org/packages/Pure.DI) source generator package.

```shell
dotnet add package Pure.DI
```

## Step 2: Add a composition class

This class setups how the composition of objects will be created for the application.

```csharp
using Pure.DI;
using static Pure.DI.Lifetime;

public partial class Composition
{
private static void Setup() => DI.Setup()
.Root<MainWindow>(nameof(MainWindow))
.Root<IMainViewModel>(nameof(MainViewModel))

.Bind().As(Singleton).To<Repository>()
.Bind().To<BusinessService>()
.Bind().As(Singleton).To<MainViewModel>();
}
```

Advantages over classical DI container libraries:
- No performance impact or side effects when creating composition of objects.
- All logic for analyzing the graph of objects, constructors and methods takes place at compile time. Pure.DI notifies the developer at compile time of missing or cyclic dependencies, cases when some dependencies are not suitable for injection, etc.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This paragraph is just an elaborated version of the previous one

- Does not add dependencies to any additional assembly.
- Since the generated code uses primitive language constructs to create object compositions and does not use any libraries, you can easily debug the object composition code as regular code in your application.

## Step 3: Modify App.axaml.cs

Next, the `App.xaml.cs` class should be modified to use the main window created in pure DI paradigm:

```csharp
public class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}

public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop
&& Resources[nameof(Composition)] is Composition composition)
{
desktop.MainWindow = composition.MainWindow;
}

base.OnFrameworkInitializationCompleted();
}
}
```

This code is only needed to create a main window in a pure DI paradigm using a shared composition instance:

```csharp
if (Resources[nameof(Composition)] is Composition composition)
{
desktop.MainWindow = composition.MainWindow;
}
```

This will be useful if the main window will require dependencies to be injected.

Advantages over classical DI container libraries:
- No explicit initialisation of data contexts is required. Data contexts are configured directly in `.axaml` files according to the MVVM approach.
Copy link
Contributor

@thevortexcloud thevortexcloud Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not clear how what you have done is any different from the MS DI example. You are still explicitly resolving things from the container, just with more steps. This also sounds like you are implying the MS DI approach violates MVVM somehow?

There is also really nothing stopping you from setting up MS DI in a similar way. It just requires slightly more effort (and also a static locator).

- The code is simpler, more compact, and requires less maintenance effort.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fairly opinionated, which I would argue does not belong in a tutorial like this. You should present facts and let people make their own decision

- The main window is created in a pure DI paradigm, and it can be easily supplied with all necessary dependencies via DI as regular types.

## Step 4: Modify App.axaml

Next, modify the `App.axaml` file to use view models according to the MVVM approach via a shared resource:

```xml
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:app="clr-namespace:AvaloniaSimpleApp"
x:Class="AvaloniaSimpleApp.App"
RequestedThemeVariant="Default">

<!-- "Default" ThemeVariant follows system theme variant.
"Dark" or "Light" are other available options. -->
<Application.Styles>
<FluentTheme />
</Application.Styles>

<Application.Resources>
<app:Composition x:Key="Composition" />
</Application.Resources>

</Application>
```

This markup fragment

```xml
<Application.Resources>
<app:Composition x:Key="Composition" />
</Application.Resources>
```

creates a shared resource of type `Composition` and with key _‘Composition’_, which will be further used as a data context in the views.

## Step 5: Create your views according to MVVM approach

You can now use bindings to model views without even editing the views `.cs` code files, for example for the main window in the example this might look like this:

```xml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
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="AvaloniaSimpleApp.MainWindow"
DataContext="{StaticResource Composition}"
xmlns:app="clr-namespace:AvaloniaSimpleApp"
x:DataType="app:Composition"
Title="{Binding MainViewModel.Title}"
Content="{Binding MainViewModel.Greetings}"/>
```

To use bindings in views:

- You can set a shared resource as a data context

`DataContext="{StaticResource Composition}"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may(?) cause side effects when running inside the previewer, as the previewer will run your real app code. You really should have a separate set of test services that sets the design time data context


The resource with key _"Composition"_ was defined in [step 4](#step-4-modify-appaxaml).

- Specify the data type in the context:

`xmlns:app="clr-namespace:AvaloniaSimpleApp"`

`x:DataType="app:Composition"`

- Use the bindings as usual:

`Title="{Binding MainViewModel.Title}"`

Advantages over classical DI container libraries:
- The code-behind `.cs` files for views are free of any logic.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are many ways to do DI without using a static locator or putting any DI logic on code behind that work in basically every DI framework. The original example code for MSDI mentioned one possible way, it was just removed due to the lack of practical examples in the guide.

https://github.com/cerobe/avalonia-docs/blob/9ea95b7540c6eaf9a59ef88776d543ecc707244a/docs/guides/implementation-guides/how-to-implement-dependency-injection.md?plain=1#L11

- This approach works just as well during design time.
- You can easily use different view models in a single view.
- Bindings depend on properties through abstractions, which additionally ensures weak coupling of types in application. This is in line with the basic principles of DI.
5 changes: 5 additions & 0 deletions docs/guides/implementation-guides/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ These guides show you how to use various implementation architectures and techni
to="/docs/guides/implementation-guides/how-to-implement-dependency-injection"
description="A guide to understanding how to use Dependency Injection (DI) in your apps."
/>
<Card
title="How To Implement Dependency Injection with Pure.DI code generator"
to="/docs/guides/implementation-guides/how-to-implement-dependency-injection-with-pure-di"
description="A guide to understanding how to use Pure DI in your apps."
/>
<Card
title="Developer Tools"
to="/docs/guides/implementation-guides/developer-tools"
Expand Down
1 change: 1 addition & 0 deletions sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ const sidebars = {
'items': [
'guides/implementation-guides/how-to-use-the-mvvm-pattern',
'guides/implementation-guides/how-to-implement-dependency-injection',
'guides/implementation-guides/how-to-implement-dependency-injection-with-pure-di',
'guides/implementation-guides/developer-tools',
'guides/implementation-guides/logging-errors-and-warnings',
'guides/implementation-guides/ide-support',
Expand Down