-
Notifications
You must be signed in to change notification settings - Fork 212
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
Enhance Dependency Injection guides #525
Comments
The original example code I wrote did in fact have a static service locator. However, I don't recall why it was removed. The reason is probably somewhere in the code review. However that version of the example really only works properly on desktop apps due to the use of the
I am not sure I understand? The example shows exactly that. If you want Avalonia to resolve objects from your DI for you, you need to write more code to glue it all together. EG a custom view locator that returns view models from the DI container. Generally using a static service locator is an anti pattern anyway.
Nope. It's just the easiest place that will work on every platform. You can do it in a few different places. EG in the
That's not true. A couple lines after you register the services shows the view model being resolved from the container and then passed to the main view. |
This is why I asked: How to make Avalonia uses my ServiceProvider when creating object instead?
public partial class App : Application
{
public static readonly IServiceProvider ServiceProvider = BuildDependencyGraph().BuildServiceProvider();
public static ServiceCollection BuildDependencyGraph()
{
ServiceCollection services = new();
services.AddLoggingService();
services.AddRouting();
services.RegisterViewModels();
services.RegisterViews();
services.RegisterBusinessLogic();
return services;
}
public override void OnFrameworkInitializationCompleted()
{
DisableAvaloniaDataAnnotationValidation();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
DataContext = ServiceProvider.GetRequiredService<MainWindowViewModel>()
};
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{
singleViewPlatform.MainView = new MainView
{
DataContext = ServiceProvider.GetRequiredService<MainViewModel>()
};
}
base.OnFrameworkInitializationCompleted();
} and for tests: [Fact]
public void WalletsViewModelTest()
{
IWalletRepository mockRepository = Substitute.For<IWalletRepository>();
ServiceCollection services = App.BuildDependencyGraph();
services.AddSingleton(mockRepository); //inject a fake repository instead of using the real repository which access to the database
IServiceProvider serviceProviderWithMockRepository = services.BuildServiceProvider();
WalletsViewModel subjectUnderTest = serviceProviderWithMockRepository.GetRequiredService<WalletsViewModel>();
subjectUnderTest.Wallets.ShouldNotBeNull();
} In conclusion, I think that the docs would be more useful if it should reference to the "Custom view locator" section, telling that "ViewLocator" is the way to glue your IOC container (a.k.a Evens more useful if it can have some insight about "MV-first" approach in the "MVVM" pattern to make codes easier to test. |
I would suggest avoiding injecting controls. They are not really meant to be used like that. The view will without warning, destroy and recreate them as needed. View models are what you use to persist state. I typically only set up view models for injection. In that situation, the example code in the docs is sufficient.
That test does not look like a valid unit test. It looks like an integration test instead. Properly designed unit tests should only be testing one specific bit of functionality. Meaning you should not be registering real services. Everything should be a fake except for the class you are testing.
The problem with view locators is they are not generally not trimming safe (due to the use of reflection). Which means they often don't work with AOT compiled apps. Which is why they were removed from the default templates. You could probably make a trim safe locator though. AvaloniaUI/avalonia-dotnet-templates#177 The locator was just one example though. There are many other ways you can use your DI container with Avalonia. It really depends on how you structure your code and what you are trying to do. EG I have written a router that will automatically resolve a view model from the DI container when something tries to display it on screen. |
Do you means that after rendering
You are right they are integration test of various ViewModels. Unit tests are rarely useful for frontend / rich interractive applications. The interesting parts which need to be tested are usually interraction between different VMs. Eg: When a "RelayCommand" is "Act" then
I sometimes write normal unit tests to cover some edge cases which is complicated to "Arrange". But I generally avoid to mock internal things of the application (We should only mock the external components.)
As long as the App works on Window, Linux, Android, and Web browser then it is good to me. In this example, I used ViewLocator and MS DI to resolve "View" and it appeared trimming safe and works on Web & Android, (not yet tested on Linux..)
Can you please show me some examples? (except the Thanks |
It wont ignore the locator. But it will query the locator again for the control. If you have poorly configured services, it can result in hard to debug crashes/memory leaks/unexpected or undefined behaviour. Similarly, if you are resolving a view model from the DI container to assign to a control in the locator, it may end up creating an entirely new instance of the view model which results in a loss of state. One other problem with adding a non default constructor for a control is it breaks the previewer. Although some people don't consider that an issue.
I might write up an example later. Most other use cases are somewhat advanced. But the limit is really your imagination due to Avalonia not using any DI out of the box. EG you can make a markup extension that resolves things from the static locator that will be accessible to your views via XAML. |
Thanks for your explanation. I will be careful for the situation which Avalonia might deliberately dispose a View then call the ViewLocator to re-create it again. I intended to borrow /experiment the store's pattern from the Web SPA's world (Vue's Pinia, Solidjs's store..) where (the whole) application state is kept in a "Reactive store" independent from the Views. About the previewer, my current work around is to add some boilerplate codes to keep it working and I'm happy with it. class MyMainViewModel(ChildViewModel childvm, IDep1 dep1, IDep2 dep2) {};
#region boilerplate codes to keep the Previewer works
class MyMainViewModelForDesigner(): MyMainViewModel {
//add a default constructor
public MyMainViewModelForDesigner(): base(new ChildViewModelForDesigner(), new Dep1ForDesigner(), null) {}
}
#endregion About the last point
Do you mean something like this? <local:ResolveControl ControlType="local:MyUserControl" /> public class ResolveControlExtension
{
public Type ControlType { get; set; }
// note, this IServiceProvider parameter IS NOT related to your configured service provider, and only used for XAML related services
public Control ProvideValue(IServiceProvider _)
{
if (!typeof(Control).IsAssignableFrom(ControlType))
throw new InvalidOperationException($"The provided type {ControlType} is not a control");
return (Control) App.ServiceProvider.GetService(ControlType);
}
} I'm new to Avalonia, and not sure if it is any diffrent from (or better? than) the ViewLocator technique: <ContentControl Content="{Binding MyUserControlViewModel}" /> public class ViewLocator : IDataTemplate
{
public Control Build(object? viewModel)
{
if (viewModel is MyUserControlViewModel)
{
return (Control)App.ServiceProvider.GetService<MyUserControl>();
}
....
}
} Comparing <local:ResolveControl ControlType="local:MyUserControl" /> vs <ContentControl Content="{Binding MyUserControlViewModel}" /> Please correct me if I'm wrong, my guess is that Avalonia would handle them the same way, it might dispose the If my guess is right then there is not really a fundamentally different.
|
There is a nicer way using a markup extension. AvaloniaUI/Avalonia#13743 (reply in thread)
It still uses the static locator. But the difference is it acts as a sort of abstraction around it, which makes it not as bad as directly using the locator and can be easily used to assign a value to another existing control. It was however just an example. A much more useful and architectural approach that I mentioned earlier would be a custom router for a SPA. I shared some of the code for one I wrote that uses DI here the other day: |
Hello,
The current example in the guide is not very helpful
https://docs.avaloniaui.net/docs/guides/implementation-guides/how-to-implement-dependency-injection
The
ServiceProvider
is created and available only inside the functionOnFrameworkInitializationCompleted()
, while it should be generally accessible to create object for us anywhere. Or How to make the avalonia framework use our ServiceProvider when creating object?Is it really neccessary to Register the services (a.k.a the
ServiceCollection
) insideOnFrameworkInitializationCompleted()
? We are registering the services, not actually create or use them, so why do we need to wait forOnFrameworkInitializationCompleted
?My suggesstion is
The text was updated successfully, but these errors were encountered: