Home > Software engineering >  WPF dependency injection when navigate to new page
WPF dependency injection when navigate to new page

Time:06-20

I have a problem because I don't know how to inject service into the page. This is how my App.xaml.cs look like

public partial class App : Application
{
    public IServiceProvider ServiceProvider { get; set; }
    public IConfiguration Configuration { get; set; }

    protected override void OnStartup(StartupEventArgs e)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json", false, true);

        Configuration = builder.Build();

        var serviceCollection = new ServiceCollection();
        ConfigureServices(serviceCollection);
        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
        ServiceProvider = serviceCollection.BuildServiceProvider();
        var mainWindow = ServiceProvider.GetRequiredService<MainWindow>();
        mainWindow.Show();
    }

    private void ConfigureServices(ServiceCollection serviceCollection)
    {
        serviceCollection.AddTransient<IPage1ViewModel, Page1ViewModel>();
        serviceCollection.AddTransient(typeof(MainWindow));
    }
}

I have MainWindow with frame, in frame I have default page called Home.xml with button.

<Window x:Class="WpfDITest.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:WpfDITest"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800">
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="30"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>

    <Menu Grid.Row="0" Grid.Column="0">
        <MenuItem Header = "Help" HorizontalAlignment="Center" VerticalAlignment="Center">
            <MenuItem Name="about" Header = "about t" HorizontalAlignment = "Stretch"/>
        </MenuItem>
    </Menu>

    <Frame Grid.Row="1" Grid.Column="0" Source="/Views/Home.xaml" NavigationUIVisibility="Hidden" />
</Grid>

When you click button it navigates you to new page called Page1.

public partial class Home : Page
{
    public Home()
    {
        InitializeComponent();
    }

  

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        var uri = "/Views/Page1.xaml";
        NavigationService.Navigate(new Uri(uri, UriKind.Relative));
    }
}

I want to inject IPage1ViewModel to my Page1 so I thought I would just inject service to constructor like in asp.net apps but the problem is Navigation service fires constructor without parametress so right now I dont know how to achieve this.

public partial class Page1 : Page
{
    private readonly IPage1ViewModel _page1ViewModel;
    public Page1(IPage1ViewModel page1ViewModel)
    {
        _page1ViewModel = page1ViewModel;
        InitializeComponent();
    }
    public Page1() //this constructor fires
    {
        InitializeComponent();
        GetData();
    }

    public void GetData()
    {
        _page1ViewModel.GetTitle(); // How to get this?
    }
}

Page1ViewModel

public class Page1ViewModel : IPage1ViewModel
{
    public Page1ViewModel()
    {

    }

    public string GetTitle()
    {
        return "Welcome";
    }
}

Is it a good idea to use dependency injection in my case? If so, how do I solve my problem?

CodePudding user response:

You would have to instantiate the Page explicitly using a factory (Abstract Factory pattern or a factory delegate).

When you instantiate controls via XAML, either by defining an element or via URI, the XAML engine will always create the instance using the default constructor (which is therefore mandatory for XAML instantiations).

If your control must use dependency injection, you must instantiate them explicitly, so that you can call the appropriate constructor, usually with the help of a factory.

  1. Make Home request a Page1 factory delegate (a Func<Page1>) as constructor dependency. Then use it to create the Page1 instance explicitly:
public partial class Home : Page
{
  private Func<Page1> Page1Factory { get; }

  public Home(Func<Page1> page1Factory)
  {
    InitializeComponent();
    this.Page1Factory = page1Factory; 
  }

  private void Button_Click(object sender, RoutedEventArgs e)
  {
    Page1 nextPage = this.Page1Factory.Invoke();
    NavigationService.Navigate(nextPage);
  }
}
  1. Register the Page1 type (with an appropriate lifetime) and its factory delegate (or alternatively an abstract factory type)
  private void ConfigureServices(ServiceCollection serviceCollection)
  {
    serviceCollection.AddTransient<IPage1ViewModel, Page1ViewModel>();
    serviceCollection.AddTransient(typeof(MainWindow));

    serviceCollection.AddTransient<Page1>();
    serviceCollection.AddSingleton<Func<Page1>>(serviceProvider => serviceProvider.GetService<Page1>);
  }
  1. Normally, you would bind the UI elements of Page1 to its DataContext. Therefore, you must set the injected view model as DataContext. Also, you should remove the default constructor (or at least make it private) as it will not initialize the type properly (the view model i.e. DataContext is missing). Additionally, a public method naked Get... is expected to return a result. Either rename the method or let it return the result or make it at least private too.
public partial class Page1 : Page
{
    private IPage1ViewModel Page1ViewModel { get; }

    // This constructor also calls the private default constructor
    // (in case this type has multiple constructor overloads. 
    // Otherwise, move the private default constructor code to this constructor).
    public Page1(IPage1ViewModel page1ViewModel) : this()
    {
      this.Page1ViewModel = page1ViewModel;
      this.DataContext = this.Page1ViewModel;
    }

    private Page1()
    {
      InitializeComponent();
      Initialize();
    }

    private void Initialize()
    {
      this.Page1ViewModel.GetTitle();
    }
}
  1. Because it is recommended to use data binding (Data binding overview (WPF .NET)), your view model classes must implement INotifyPropertyChanged (see Microsoft docs for an example
  • Related