Open marwalsch opened 3 years ago
I am surprised this is a requirement from the community, I thought it is like that by default as they added dependency injection! so why did we add dependency injection if I still need to pass dependencies 😅. I also suggest an InstanceFactory.Create<T>()
to create any object without dependencies
@MhAllan There is dependency injection, either through the default IServiceProvider
from .NET6 used by Maui or the DependencyServide
used by Xamarin earlier, though they do use different containers.
The issue is that the overloads of the Push
methods currently only take instances which hinders constructor injection.
@marwalsch your request should be for InstanceFactory.Create<T>()
not for Navitation.Push<T>()
because that dos not give you control on how many instances of the given page you want as it will create instance for every Push. while if you have InstanceFactory.Create<T>
you are free to re-use the same page to push again or create pages in advance and also create any object not only pages. I am suggesting InstanceFactory
but simply we can expose whatever Maui uses to create instances so we have access to it anywhere in the app
Yes it does: MauiApp.CreateBuilder().Services.AddSingleton<T>()
;
Calling CreateInstance
explicitly to pass dependencies is actually an Anti-Pattern and forces you to have a Scoped/Transient lifetime.
@marwalsch that InstanceFactory.Create<T>()
is not for creating instances of the dependencies, it is for creating the instance that you need, like a Page
@MhAllan That's IServiceProvider.GetService()
. With the proposal's implementation there is no need to call for instances explicitly at all.
@marwalsch I think I understand your concern now, we can register pages as services so why do we need CreateInstace
.. hmm, right
This might be occasion to reconsider how Page instantiation through the framework works. Features like default navigations currently instantiate through the parameterless constructor of a page, which worked with the legacy(?) DependencyService
but not with constructor injection.
Markup like <FlyoutPageItem TargetType="{x:Type page:SomePage}" />
currently not only makes this impossible, but also is counter-intuitive as the Type should suffice.
Yes it does:
MauiApp.CreateBuilder().Services.AddSingleton<T>()
;Calling
CreateInstance
explicitly to pass dependencies is actually an Anti-Pattern and forces you to have a Scoped/Transient lifetime.
@marwalsch I agree with your sentiment in general, however, I'd still like to see a way to pass in constructor parameters (dependencies) to the "Create" method. It's useful for situations where you're binding to collections of objects/data and you're using a DataTemplateSelector
to convert the collection bound items into Views with companion ViewModels. For example, any implementation of ItemsView.ItemsSource.
E.g.
Without getting into specifics on the objects/data in a collection, consider the pattern where you're using a CollectionView
and binding it's ItemSource
property to that collection. When you specify a DataTemplateSelector
implementation you can customize the view for each individual element of the collection based upon it's type. When that Template is returned from DataTemplateSelector.OnSelectTemplate
the object/data value that was provided to to OnSelectTemplate
is applied to the newly created View's BindingContext
property. And there's the issue; we want to have a ViewModel wrapped around that object/data value for our View to reference, not the raw value. If we have access to the DI Resolver which supports a specified constructor parameter we can use Binding (with a Converter
and ConverterParameter
specified) or create a custom IMarkupExetension
to support the creation of the ViewModel using THE object/data value from the collection in its constructor.
This pattern allows dynamic generation of ViewModels for object/data values in hierarchical data patterns in bound collections by eliminating lots of extra code work in a top level ViewModel. Which would otherwise be responsible for creating all the ViewModels up front in separate collections, and also managing any/all updates to the VM collections when data in the source collection changes.
Some rudimentary code might help illustrate:
<StackLayout Margin="10">
<CollectionView ItemTemplate="{x:Static views:SettingDataTemplateSelector.Instance}"
ItemsSource="{Binding Settings}" />
</StackLayout>
the DataTemplateSelector:
internal class SettingDataTemplateSelector : DataTemplateSelector
{
public static readonly DataTemplateSelector Instance =
new Lazy<DataTemplateSelector>( () => new SettingDataTemplateSelector() ).Value;
private static readonly DataTemplate SwitchTemplate =
new Lazy<DataTemplate>( () => new DataTemplate( () => new SwitchView() ) ).Value;
private static readonly DataTemplate EntryTemplate =
new Lazy<DataTemplate>(() => new DataTemplate(() => new EntryView() ) ).Value;
protected override DataTemplate OnSelectTemplate( object item, BindableObject container )
{
switch ( item )
{
default:
case SwitchSetting _:
return SettingDataTemplateSelector.SwitchTemplate;
case ValueSetting<int> _:
case ValueSetting<double> _:
case ValueSetting<byte> _:
case StringSetting _:
return SettingDataTemplateSelector.EntryTemplate;
}
}
}
and the TemplatedView, where the IValueConverter.Convert
method creates the ViewModel viewModels:DataSpecificViewModel
by passing the actual value into the DI Resolver to be used in the constructor of the type.
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Hypertherm.Settings.Views.EntryView">
<!-- Placing the Binding on BindingContext at the StackPanel level causes the binding to be applied BEFORE the
data is propagated to the children. Thus the children only ever see the ViewModel in their BindingContext -->
<StackPanel Padding="10, 0"
BindingContext="{Binding Mode=OneTime, Converter={x:Static uxConverters:ViewModelActivator.Instance}, ConverterParameter={ x:Type viewModels:DataSpecificViewModel}}">
<!-- All children see the dynamically created ViewModel -->
</StackPanel>
</ContentView>
I've had lots of success with this pattern using DryIoc with Prism. In fact, in the Prism implementation I can supply 0 - N constructor arguments (dependencies) and DriIoc will resolve the remaining unspecified ones, if I've not specified them all. Thus I can have the best of both worlds, I can specify the Object/Data dependency for the ViewModel, and let the Ioc Container look up any services that VM might also require.
Description
Currently, the framework requires instantiation of a
ContentPage
object in order to navigate. This is quite obstructive when using constructor injection, especially when using MVU or lightweight patterns relying mostly on logic inside the code-behind. This has been addressed in this discussion and @matt-goldman actually provided a very lightweight and straightforward solution . I suggest considering on adopting it into the framework's existingNavigation
. Blazor fell back to property injection which might not be ideal but at least provided a feasible solution.Public API Changes
Check the repository's README for reference.
Intended Use-Case
Currently:
await Navigation.PushModalAsync(new MyPage(/*requires dependencies*/));
Goldman's solution:
await Navigation.PushModalAsync<MyPage>(); // built-in container resolves dependencies