-
Notifications
You must be signed in to change notification settings - Fork 29
Beginner Question about implementing viewmodels #53
-
I have a Window that has a viewmodel, that has a shared service that uses another shared service.
using Microsoft Extensions DI, I'd have something like
private static IServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
services.AddSingleton<ITodoDataProvider, DummyTodoDataProvider>();
services.AddSingleton<TodoService>();
services.AddSingleton<TinyTaskWindowViewModel>();
return services.BuildServiceProvider();
}
In the constructor for the window, I'd write
Ioc.Default.GetService();
and for the rest, just have the service as a parameter.
How would I do this in Pure.DI?
There's not many open sourceprojects using Pure.DI so I can't find many examples to learn from.
Beta Was this translation helpful? Give feedback.
All reactions
I didn't quite understand the question about composition roots. Let me tell you a little bit why composition roots are very important in Pure.DI and not so important in classic DI containers.
If you use classic DI containers, the composition is created dynamically every time you call a method similar to T Resolve<T>() or object GetService(Type type). The root of the composition there is simply the root type of the composition of objects in memory T or Type type. There can be as many of these as you like.
In the case of Pure.DI, the number of composition roots is limited because for each composition root a separate property or method is created at compile time. Therefore, each root is defi...
Replies: 3 comments 8 replies
-
@AlexanderBlackman You can do exactly the same thing:
using Pure.DI; var window = new MyWindows(); interface ITodoDataProvider; class DummyTodoDataProvider: ITodoDataProvider; class TodoService; class TinyTaskWindowViewModel; class MyWindows { private readonly TinyTaskWindowViewModel _viewModel; public MyWindows() { _viewModel = MyComposition.Default.Resolve<TinyTaskWindowViewModel>(); } } partial class MyComposition { public static readonly MyComposition Default = new(); private void Setup() => DI.Setup(nameof(MyComposition)) .Bind().As(Lifetime.Singleton).To<DummyTodoDataProvider>() .Bind().As(Lifetime.Singleton).To<TodoService>() .Bind().As(Lifetime.Singleton).To<TinyTaskWindowViewModel>() .Root<TinyTaskWindowViewModel>(); }
But this is not the best solution since your window will obviously create a view model using ServiceLocator's anti-pattern and will be aware of DI. It would be better if your view layer would know less - depend only on abstraction and not know about infrastructure. As far as I understand you are experimenting on a desktop application. Is it MAUI, WPF, Avalonia, WinForms or something else? There are some examples of using Pure.DI for different types of applications. Using a WPF application as an example the basic idea is as follows. You define a composition as a resource right in the markup:
<Application x:Class="WpfAppNetCore.App" x:ClassModifier="internal" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:wpfAppNetCore="clr-namespace:WpfAppNetCore" StartupUri="/Views/MainWindow.xaml" Exit="OnExit"> <Application.Resources> <wpfAppAppNetCore:Composition x:Key="Composition"/> <Application.Resources> </Application>
And then use bindings to access the view model:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" DataContext="{StaticResource Composition}" Title="{Binding ClockViewModel.Time}"> <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" DataContext="{Binding ClockViewModel}"> <TextBlock Text="{Binding Date}" FontSize="64" HorizontalAlignment="Center"/> <TextBlock Text="{Binding Time}" FontSize="128" HorizontalAlignment="Center"/> </StackPanel> </Window>.
With this approach, all layers of the application remain as independent as possible:
- Your application logic knows nothing about DI
- The application is divided into 3 layers in MVVM concept and each layer depends only on abstractions
- As a bonus of this approach - you can more easily do application design in the IDE :)
If you share what kind of application you are interested in we can discuss in more detail.
Beta Was this translation helpful? Give feedback.
All reactions
-
I was using Avalonia, but the syntax is pretty much the same.
I found samples.AvaloniaApp hard to apply, as I was so use to the Winui + MS DI way of doing things.
Looking over it again, Log TT is an injected service and Clock.Models.Timer is an implementation of ClockViewModel's required ITimer interface. I'll try again in the morning and get back to you.
I managed to put together a ViewLocator source generator* for another multipage app, but had little luck injecting non-view services, so I'm trying to use Pure.DI.
Thank you for your answer.
Beta Was this translation helpful? Give feedback.
All reactions
-
Yes you are right the approach to Avalonia is the same. If we want to do MVVM:
- In the App.axam file, register the resource:
<Application.Resources> <local:Composition x:Key="Composition" /> <Application.Resources>
- And then use it everywhere at the view level - bind the desired view model using data context, for example in the MainWindow.axaml file:
<Window xmlns="https://github.com/avaloniaui" ... x:DataType="avaloniaApp:Composition" DataContext="{StaticResource Composition}" ...> <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" DataContext="{Binding ClockViewModel}"> <TextBlock Text="{Binding Date}" FontSize="64" HorizontalAlignment="Center" /> <TextBlock Text="{Binding Time}" FontSize="128" HorizontalAlignment="Center" /> </StackPanel> </Window>
- When customizing the composition, you need to remember to define the roots of the preposition models as:
.Root<IClockViewModel>("ClockViewModel")
because unlike classical DI container libraries, to optimize code generation, their number should be as small as possible and therefore each root should be defined explicitly. It is better to have only one at all. But in the example 2 are created for the sake of example.
Feel free to ask. If you create a repository with your experiences I can offer solutions in the form of PR.
Beta Was this translation helpful? Give feedback.
All reactions
-
I've done a lot of reading and experimentation today. I've managed to make compositions which inject a service simply by having an abstract in the constructor. Your library is very useful.
I'm a bit confused about the composition root. I know the basic theory, but looking though the readme it appears there is a root for each parent service.
Should I just have a Root for each window, and bind all services used by the children? I guess it'd make sense, as they often share the same resources.
Cheers
Beta Was this translation helpful? Give feedback.
All reactions
-
I didn't quite understand the question about composition roots. Let me tell you a little bit why composition roots are very important in Pure.DI and not so important in classic DI containers.
If you use classic DI containers, the composition is created dynamically every time you call a method similar to T Resolve<T>() or object GetService(Type type). The root of the composition there is simply the root type of the composition of objects in memory T or Type type. There can be as many of these as you like.
In the case of Pure.DI, the number of composition roots is limited because for each composition root a separate property or method is created at compile time. Therefore, each root is defined explicitly by calling the Root(string rootName) method.
Regarding the question that each window should have its own composition root. In example for Avalonia , the view model has its own root. This is done for simplicity in the example. There are several alternatives, such as:
- Composition root per view model
- Composition root per window
- A composition root for the entire application
I think there is no best option here, each will be more applicable in one case or another. You just have to remember:
- the more roots - the more code will be generated to create compositions of objects
- the lifetime of objects will be applied differently, for example for PerResolve lifetime in case of one root for the whole application, it will be necessary to use Signleton in case of several roots of the composition
I would prefer a single composition root for the whole application. To do this, you could create for example a "synthetic" AppDataContext class, which would contain one property per view model. If the view model should be created lazily you can use Lazy<TViewModel>, if there should be many of them you can use Func<TViewModel>.
But this is all at your discretion. The main thing is that Pure.DI allows you to use DI in the whole application. The application code does not know about DI. And it all fits well into MVVM.
Beta Was this translation helpful? Give feedback.
All reactions
-
Added an example of a single root for the Avalonia application
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 2
-
Sorry to bother you yet again. Getting "Cannot parse a compiled binding without an explicit datatype..." errors. And if I use uncompiled bindings, nothing loads.
I'm finding it hard to get working, maybe my vanilla Avalonia knowledge is just too limited. I'm thinking about giving up and using Winui3/M.E.DI instead.
That being said, here's my current composition and AppDataContext class.
internal partial class Composition
{
void Setup() => DI.Setup(nameof(Composition))
.DefaultLifetime(Singleton)
.Bind<ITodoDataProvider>().To<DummyTodoDataProvider>()
.Bind<ITodoService>().To<TodoService>()
.Bind<MainWindowView>().To<MainWindowView>()
.Bind<TinyTaskWindowView>().To<TinyTaskWindowView>()
.Bind().To<TinyTaskWindowViewModel>()
.Bind().To<MainWindowViewModel>()
.Root<AppDataContext>("Root");
}
public class AppDataContext(TinyTaskWindowView tinyTaskWindowView,
MainWindowView mainWindowView,
TinyTaskWindowViewModel tinyTaskWindowViewModel,
MainWindowViewModel mainWindowViewModel)
{
public TinyTaskWindowView TinyTaskWindow => tinyTaskWindowView;
public MainWindowView MainWindow => mainWindowView;
public TinyTaskWindowViewModel TinyTaskVM => tinyTaskWindowViewModel;
public MainWindowViewModel MainVM => mainWindowViewModel;
}
From app.axaml.cs
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop
&& Resources["Composition"] is Composition compo
)
{
CurrentWindow = compo.Root.MainWindow;
TinyTaskWin = compo.Root.TinyTaskWindow;
desktop.MainWindow = CurrentWindow;
TinyTaskWin.Activate();
xaml:
<Application.Resources>
<local:Composition x:Key="Composition"/>
</Application.Resources>
}
And here is MainWindowView
xmlns:app="clr-namespace:Rzy.AVA"
x:DataType="app:Composition"
DataContext="{Binding Root.MainVM}">
<Design.DataContext>
<vm:MainWindowViewModel/>
</Design.DataContext>
<TextBlock Text="{Binding Greeting}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
Beta Was this translation helpful? Give feedback.
All reactions
-
No worries, every such question helps to identify questionable use cases of Pure.DI.
I assume the problem is in the xaml:
x:DataType="app:Composition"
DataContext="{Binding Root.MainVM}">I'm not sure you've defined the data context correctly here. In the example, the data context is defined by an instance of the Composition class from the resources.
I think these types in DataType and in must DataContext are the same.
If you took the example as a basis, there:
x:DataType="avaloniaApp:Composition"
DataContext="{StaticResource Composition}"DataContext="{StaticResource Composition}" means to take a generic instance of the Composition class from the resources as the data context. x:DataType="avaloniaApp:Composition" - means to use compiled bindings of the specified type. Further all bindings can be performed based on the data context.
If you share your example I will try to find the problem exactly.
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
My last experience with WPF was 10 years ago, and I'm only familiar with Avalonia from simple examples :)
Beta Was this translation helpful? Give feedback.
All reactions
-
Another thing is in the example:
internal class AppDataContext( Lazy<MainWindow> mainWindow, IClockViewModel clockViewModel) { public MainWindow MainWindow => mainWindow.Value; public IClockViewModel ClockViewModel => clockViewModel; }
The mainWindow is Lazy<MainWindow>, since the root of the composition will be created as a static resource when the main window cannot be created yet. Therefore the main window will be created lazily only when it is actually needed.
Beta Was this translation helpful? Give feedback.