microsoft / microsoft-ui-xaml

WinUI: a modern UI framework with a rich set of controls and styles to build dynamic and high-performing Windows applications.
MIT License
6.39k stars 683 forks source link

Proposal: Instantiate Resources from Dependency Injection #1625

Open sharpninja opened 5 years ago

sharpninja commented 5 years ago

Proposal: Instantiate Resources from Dependency Injection

Summary

Use Microsoft.Extensions.DependencyInjection or other framework that implements IServiceProvider to instantiate resources to instantiate Resources from XAML..

Rationale

Scope

Capability Priority
This proposal will allow developers to use design time bindings on View Models and other objects created via Dependency Injection Must
This proposal will allow developers to use any dependency injection framework that implements IServiceProvider. Must
This proposal will allow developers to define an Interface as a Resource since the instance will be obtained from the IServiceProvider. Must
This proposal will not require dependency injection for all projects. Must

Important Notes

Example

<local:IMyResource x:Key="MyResource" ServiceProvider="{x:Bind Provider}" />

Here, ServiceProvider is any instance of IServiceProvider that can be resolved via x:Bind.

jesbis commented 5 years ago

@sharpninja to make sure I understand, would your goal be to replace codebehind like this that would register a service instance (which should work with Xaml types today even with non-default constructors):

var services = new ServiceCollection();
services.AddSingleton<IMyResource>(MyResource);
this.Provider = services.BuildServiceProvider();

with your example markup?

sharpninja commented 5 years ago

@jebris - You would indeed still need to register the object as you listed so that XAML gets instances of that object from the IServiceProvider specified in the XAML. And this really can extend beyond just resources to all objects such that an entire object try in XAML could be constructed from DI.

jesbis commented 5 years ago

So is your proposal that this markup:

<local:IMyResource x:Key="MyResource" ServiceProvider="{x:Bind Provider}" />

would automatically register that IMyResource instance with the referenced ServiceProvider?

Would that ServiceProvider instance be created by the framework or the app? If it was created by the app, how would you propose that the Xaml framework get the associated ServiceCollection and rebuild the ServiceProvider?

A more complete example might also help clarify if you could provide one 😊

sharpninja commented 5 years ago

You would still need this:

Startup.cs

var services = new ServiceCollection();
services.AddTransient<SomeTemplate, SomeTemplate>();
services.AddSingleton<IMainPage, MainPage>();
services.AddSingleton<IMainPageViewModel, MainPageViewModel>();
services.AddSingleton<ISomeDataModel, SomeDataModel>();
App.Provider = services.BuildServiceProvider();

Once all of the DI is defined, your code can look like this.

SomeDataModel.cs

    public ILogger Logger { get; }
    public SomeDataModel(ILogger<SomeDataModel> logger)
    {
        logger.LogDebug("Constructing SomeDataModel ...");

        Logger = logger;
    }

MainPageViewModel.cs

    private IServiceProvider _serviceProvider = null;

    public ILogger Logger { get; }
    public IConfiguration Configuration { get; }
    public IServiceProvider Provider = _serviceProvider ??= App.Current.Resources["Provider"] as IServiceProvider;

    public MainPageViewModel(ILogger<MainPageViewModel> logger
        , IConfiguration configuration)
    {
        logger.LogDebug("Constructing MainPageViewModel ...");

        Logger = logger;
        Configuration = configuration;
    }

    public void LoadData()
    {
        Logger.LogDebug("Loading Data ...");
        // use Configuration to load SomeData
        var current = Provider.GetService<ISomeDataModel>();
        var current.Populate(...);
        SomeData.Add(current)
    }

    public ObservableCollection<ISomeDataModel> SomeData { get; } =
        new ObservableCollection<ISomeDataModel>();

That would allow this:

App.xaml

<system:IServiceProvider x:Key="Provider" ServiceProvider="{x:Bind ServiceProvider}" />
<local:SomeTemplate x:Key="SomeTemplate" ServiceProvider="{StaticResource Provider}" />

App must expose an instance of IServiceProvider named ServiceProvider that can be resolved by x:Bind once.

MainPage.xaml

  <Page.DataContext>
    <local:MainPageViewModel ServiceProvider="{StaticResource Provider}"/>
  </Page.DataContext>
  ...
  <ListView ItemSource="{x:Bind SomeData}" ItemTemplate="{StaticResource SomeTemplate}" />

SomeTemplate.xaml

<DataTemplate x:DataType="local:SomeDataModel">
   ...
</DataTemplate>

SomeTemplate.xaml.cs

    public ILogger Logger { get; }
    public SomeTemplate(ILogger<SomeTemplate> logger)
    {
        logger.LogDebug("Constructing SomeTemplate ...");

        Logger = logger;
        InitializeComponent();
    }

    public SomeDataModel DataModel 
    { 
        get => DataContext as SomeDataModel;
        set => DataContext = value;
    }

Then in your code you can have all of your Pages and custom Controls be instantiated with Dependency Injection. In the case of MainPage, it can now be created with a constructor that is populated from dependency injection instead of a default constructor. No initialization method would be necessary.

MainPage.xaml.cs

    private MainPageViewModel _viewModel = null;
    public MainPageViewModel ViewModel => _viewModel ??= DataContext as MainPageViewModel;

    public MainPage(ILogger<MainPage> logger)
    {
        logger.LogDebug("Constructing MainPage ...");
        InitializeComponent();

        Loaded += (s, a) => ViewModel.LoadData();
    }
jesbis commented 4 years ago

Thanks for the full example! That's much clearer.

This seems worth considering. It's unlikely we'll be able to get to this for the first WinUI 3.0 release, so we'll keep it in the backlog for a future iteration.

sharpninja commented 4 years ago

Thanks! If any tasks become available where I could assist I'd be happy to.

pjmagee commented 3 years ago

Maui has taken an approach that uses the Microsoft.Extensions.Hosting pattern. I thought that something similar would have been done for WinUI 3.0.

Allowing developers to wire up services, background services, view models etc. Has there been any further internal discussions around how a WinUI 3.0 .NET Project is bootstrapped?

sharpninja commented 3 years ago

Allowing developers to wire up services, background services, view models etc. Has there been any further internal discussions around how a WinUI 3.0 .NET Project is bootstrapped?

I've actually been doing this by setting the App.xaml file to a Page and adding Program.cs to the project and building up a default host. I wrap the Application in a simple IHostedService and register it with the Host which manages the GUI lifecycle.

pjmagee commented 3 years ago

Allowing developers to wire up services, background services, view models etc. Has there been any further internal discussions around how a WinUI 3.0 .NET Project is bootstrapped?

I've actually been doing this by setting the App.xaml file to a Page and adding Program.cs to the project and building up a default host. I wrap the Application in a simple IHostedService and register it with the Host which manages the GUI lifecycle.

Nice, do you have a simple skeleton to share by any chance?

sharpninja commented 3 years ago

Nice, do you have a simple skeleton to share by any chance?

https://github.com/sharpninja/WindowsAppSdkHost