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.
- Make
Home
request aPage1
factory delegate (aFunc<Page1>
) as constructor dependency. Then use it to create thePage1
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);
}
}
- 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>);
}
- Normally, you would bind the UI elements of
Page1
to itsDataContext
. Therefore, you must set the injected view model asDataContext
. Also, you should remove the default constructor (or at least make itprivate
) as it will not initialize the type properly (the view model i.e.DataContext
is missing). Additionally, apublic
method nakedGet...
is expected to return a result. Either rename the method or let it return the result or make it at leastprivate
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();
}
}
- 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