UWP Template 10 and Service Dendency Injection (MVVM) not WPF

4.8k views Asked by At

I have spent over two weeks searching google, bing, stack overflow, and msdn docs trying to figure out how to do a proper dependency injection for a mobile app that I am developing. To be clear, I do DI every day in web apps. I do not need a crash course on what, who, and why DI is important. I know it is, and am always embracing it.

What I need to understand is how this works in a mobile app world, and in particular a UWP Template 10 Mobile app.

From my past, in a .net/Asp app I can "RegisterType(new XYZ).Singleton() blah" {please forgive syntax; just an example} in App_Start.ConfigureServices. This works almost identical in .netcore, granted some syntactic changes.

My problem is now I am trying to provide my api is going to an UWP app that needs to digest my IXYZ service. By no means do I think that they should "new" up an instance every time. There has to be a way to inject this into a container on the UWP side; and I feel I am missing something very simple in the process.

Here is the code I have:

App.xaml.cs

public override async Task OnStartAsync(StartKind startKind, IActivatedEventArgs args)
    {
        // TODO: add your long-running task here

        //if (args.Kind == ActivationKind.LockScreen)
        //{

        //}
        RegisterServices();
        await NavigationService.NavigateAsync(typeof(Views.SearchCompanyPage));

    }

public static IServiceProvider Container { get; private set; }

private static void RegisterServices()
{
    var services = new ServiceCollection();
    services.AddSingleton<IXYZ, XYZ>();
    Container = services.BuildServiceProvider();
}

MainPage.xaml.cs:

public MainPage()
{
   InitializeComponent();
   NavigationCacheMode = NavigationCacheMode.Enabled;
}

MainPageViewModel:

public class MainPageViewModel : ViewModelBase
{
    private readonly IXYZ _xyz;
    public MainPageViewModel(IXYZ xyz)
    {
        //Stuff
        _xyz= xyz;
    }

}

I now get the error: XAML MainPage...ViewModel type cannot be constructed. In order to be constructed in XAML, a type cannot be abstract, interface nested generic or a struct, and must have a public default constructor.

I am willing to use any brand of IoC Container, but what I need is an example of how to properly use DI for services in a UWP app. 99.9% of questions about DI is about Views (i.e. Prism?) not just a simple DI for a service (i.e. DataRepo; aka API/DataService).

Again, I feel I am missing something obvious and need a nudge in the right direction. Can somebody show me an example project, basic code, or a base flogging on how I should not be a programmer...please don't do that (I don't know if my ego could take it).

2

There are 2 answers

0
rabidwoo On

With help from @mvermef and the SO question Dependency Injection using Template 10 I found a solutions. This turned out to be a rabbit hole where at every turn I ran into an issue.

The first problem was just getting Dependency Injection to work. Once I was able to get that figured out from the sources above I was able to start injecting my services into ViewModels and setting them to the DataContext in the code behind.

Then I ran into an injection issue problem with injecting my IXYZ services into the ViewModels of UserControls.

Pages and their ViewModels worked great but I had issues with the DataContext of the UserControl not being injected with UserControl's ViewModel. They were instead getting injected by the Page's ViewModel that held it.

The final solution turned out to be making sure that the UserControl had the DataContext being set in XAML not the code behind, as we did with the Pages, and then creating a DependencyProperty in the code behind.

To show the basic solution read below.

To make it work I started with:

APP.XAML.CS

public override async Task OnStartAsync(StartKind startKind, IActivatedEventArgs args)
{
        // long-running startup tasks go here
       RegisterServices();
       await Task.CompletedTask;
}

private static void RegisterServices()
{
        var services = new ServiceCollection();
        services.AddSingleton<IRepository, Repository>();
        services.AddSingleton<IBinderService, BinderServices>();

        **//ViewModels**
        **////User Controls**
        services.AddSingleton<AddressesControlViewModel, AddressesControlViewModel>();
        services.AddSingleton<CompanyControlViewModel, CompanyControlViewModel>();

        **//ViewModels**
        **////Pages**
        services.AddSingleton<CallListPageViewModel, CallListPageViewModel>();
        services.AddSingleton<CallListResultPageViewModel, CallListResultPageViewModel>();
        etc....

        Container = services.BuildServiceProvider();
}

public override INavigable ResolveForPage(Page page, NavigationService navigationService)
{
       **//INJECT THE VIEWMODEL FOR EACH PAGE**
       **//ONLY THE PAGE NOT USERCONTROL**
        if (page is CallListPage)
        {
            return Container.GetService<CallListPageViewModel>();
        }
        if (page is CallListResultPage)
        {
            return Container.GetService<CallListResultPageViewModel>();
        }

        etc...
        return base.ResolveForPage(page, navigationService);
    }

In the code behind for the Page

CALLLISTPAGE.XAML.CS

public CallListPage()
    {
        InitializeComponent();
    }

    CallListPageViewModel _viewModel;

    public CallListPageViewModel ViewModel
    {
        get { return _viewModel ?? (_viewModel = (CallListPageViewModel)DataContext); }
    }

In your XAML add your UserControl

CALLLISTPAGE.XAML

<binder:CompanyControl Company="{x:Bind ViewModel.SelectedCompany, Mode=TwoWay}"/>

In your UserControl make sure to add the DataContext to the XAML NOT the code behind like we did with the pages.

COMPANYCONTROL.XAML

<UserControl.DataContext>
    <viewModels:CompanyControlViewModel x:Name="ViewModel" />
</UserControl.DataContext>

In the UserControl Code Behind add a Dependency Property

COMPANYCONTROL.XAML.CS

    public static readonly DependencyProperty CompanyProperty = DependencyProperty.Register(
        "Company", typeof(Company), typeof(CompanyControl), new PropertyMetadata(default(Company), SetCompany));

    public CompanyControl()
    {
        InitializeComponent();
    }

    public Company Company
    {
        get => (Company) GetValue(CompanyProperty);
        set => SetValue(CompanyProperty, value);
    }

    private static void SetCompany(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var control = d as CompanyControl;
        var viewModel = control?.ViewModel;
        if (viewModel != null)
            viewModel.Company = (Company) e.NewValue;
    }

In the end I am not sure if this is an elegant solution but it works.

0
alejandro-ordonez On

You can try to Microsoft.Hosting.Extensions just like ASP.NET, there's an implementation on Xamarin.Forms by James Montemagno, as well it can be used in UWP I have tried and it works perfectly. You have to change some parts in order to get it working.

In OnLaunched Method add Startup.Init();

    public static class Startup
    {
        public static IServiceProvider ServiceProvider { get; set; }
        public static void Init()
        {
            StorageFolder LocalFolder = ApplicationData.Current.LocalFolder;
            var configFile = ExtractResource("Sales.Client.appsettings.json", LocalFolder.Path);

            var host = new HostBuilder()
                        .ConfigureHostConfiguration(c =>
                        {
                            // Tell the host configuration where to file the file (this is required for Xamarin apps)
                            c.AddCommandLine(new string[] { $"ContentRoot={LocalFolder.Path}" });

                            //read in the configuration file!
                            c.AddJsonFile(configFile);
                        })
                        .ConfigureServices((c, x) =>
                        {
                            // Configure our local services and access the host configuration
                            ConfigureServices(c, x);
                        }).
                        ConfigureLogging(l => l.AddConsole(o =>
                        {
                            //setup a console logger and disable colors since they don't have any colors in VS
                            o.DisableColors = true;
                        }))
                        .Build();

            //Save our service provider so we can use it later.
            ServiceProvider = host.Services;
        }

        static void ConfigureServices(HostBuilderContext ctx, IServiceCollection services)
        {
            //ViewModels
            services.AddTransient<HomeViewModel>();
            services.AddTransient<MainPageViewModel>();
        }

        static string ExtractResource(string filename, string location)
        {
            var a = Assembly.GetExecutingAssembly();
            
            using (var resFilestream = a.GetManifestResourceStream(filename))
            {
                if (resFilestream != null)
                {
                    var full = Path.Combine(location, filename);

                    using (var stream = File.Create(full))
                    {
                        resFilestream.CopyTo(stream);
                    }
                }
            }
            return Path.Combine(location, filename);
        }
    }

Injecting a ViewModel is possible as well which is pretty nice.