injecting viewmodel class without parameterless constructor in WPF with NInject
Asked Answered
D

2

5

I'm using NInject to resolve the dependency for my first WPF application. Following are my code snippets.

My App.xaml.cs goes like.

public partial class App : Application
{
    private IKernel container;

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        ConfigureContainer();
        ComposeObjects();
    }

    private void ComposeObjects()
    {
        Current.MainWindow = this.container.Get<MainWindow>();
    }

    private void ConfigureContainer()
    {
        this.container = new StandardKernel();
        container.Bind<ISystemEvents>().To<MySystemEvents>();

    }
}

App.xaml goes like this.

<Application x:Class="Tracker.App"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Application.Resources>

    </Application.Resources>
</Application>

MainWindow.xaml.

<Window x:Class="Tracker.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:viewmodel="clr-namespace:Tracker.ViewModel"
        Title="MainWindow" Height="150" Width="350">
    <Window.DataContext>
        <viewmodel:TrackerViewModel>
        </viewmodel:TrackerViewModel>
    </Window.DataContext>
    <Grid>
    </Grid>
</Window>

MainWindow.xaml.cs

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
}

and viewmodel

internal class TrackerViewModel : System.ComponentModel.INotifyPropertyChanged
{
    public TrackerViewModel(ISystemEvents systemEvents)
    {
        systemEvents.SessionSwitch += SystemEvents_SessionSwitch;
    }

    private void SystemEvents_SessionSwitch(object sender, SessionSwitchEventArgs e)
    {
    }
}

Now when I launch the application, I get an exception An unhandled exception of type 'System.NullReferenceException' occurred in PresentationFramework.dll in InitializeComponent() method.

I know its because of the viewmodel class not have parameterless constructor. But I am not able to undestand why dependency injector is not able to resolve this? Am I doing something wrong?

Any help would be greatly appreciated.

Divot answered 13/9, 2015 at 9:41 Comment(1)
When you define a custom constructor, the default constructor is silently removed from your class. So, if any other component needs it, dependency injector or else, it will not be able to resolve it. So, you must explicitly define your parameterless constructor.Deen
K
8

First of all, I recommend reading the book Dependency Injection in .NET, especially the section about WPF. But even if you don't read it, there is a helpful example in the code download for the book.


You have already worked out that you need to remove the StartupUri="MainWindow.xaml" from your App.xaml file.

However, when using DI you must not wire up the DataContext declaratively otherwise it will only be able to work with the default constructor.

<Window x:Class="WpfWithNinject.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="150" Width="350">

</Window>

The pattern that is used in WPF is a bit confusing when it comes to DI. The main issue is that if you want your ViewModel to be able to control its own windowing environment, there is a circular dependency issue between the MainWindow and its ViewModel, so you will need to make an Abstract Factory in order to instantiate the ViewModel so the dependencies can be satisfied.

Creating the ViewModel Factory

internal interface ITrackerViewModelFactory
{
    TrackerViewModel Create(IWindow window);
}

internal class TrackerViewModelFactory : ITrackerViewModelFactory
{
    private readonly ISystemEvents systemEvents;

    public TrackerViewModelFactory(ISystemEvents systemEvents)
    {
        if (systemEvents == null)
        {
            throw new ArgumentNullException("systemEvents");
        }

        this.systemEvents = systemEvents;
    }

    public TrackerViewModel Create(IWindow window)
    {
        if (window == null)
        {
            throw new ArgumentNullException("window");
        }

        return new TrackerViewModel(this.systemEvents, window);
    }
}

The TrackerViewModel also needs to have some rework so it can accept the IWindow into its constructor. This allows the TrackerViewModel to control its own windowing environment, such as showing modal dialog boxes to the user.

internal class TrackerViewModel : System.ComponentModel.INotifyPropertyChanged
{
    private readonly IWindow window;

    public TrackerViewModel(ISystemEvents systemEvents, IWindow window)
    {
        if (systemEvents == null)
        {
            throw new ArgumentNullException("systemEvents");
        }
        if (window == null)
        {
            throw new ArgumentNullException("window");
        }

        systemEvents.SessionSwitch += SystemEvents_SessionSwitch;
        this.window = window;
    }

    private void SystemEvents_SessionSwitch(object sender, SessionSwitchEventArgs e)
    {
    }

    public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
}

Adapting the Window

You need to fix up the framework a bit with an abstract type for the windows, IWindow, and an abstraction to help manage DI of each of the windows, WindowAdapter.

internal interface IWindow
{
    void Close();

    IWindow CreateChild(object viewModel);

    void Show();

    bool? ShowDialog();
}

internal class WindowAdapter : IWindow
{
    private readonly Window wpfWindow;

    public WindowAdapter(Window wpfWindow)
    {
        if (wpfWindow == null)
        {
            throw new ArgumentNullException("window");
        }

        this.wpfWindow = wpfWindow;
    }

    #region IWindow Members

    public virtual void Close()
    {
        this.wpfWindow.Close();
    }

    public virtual IWindow CreateChild(object viewModel)
    {
        var cw = new ContentWindow();
        cw.Owner = this.wpfWindow;
        cw.DataContext = viewModel;
        WindowAdapter.ConfigureBehavior(cw);

        return new WindowAdapter(cw);
    }

    public virtual void Show()
    {
        this.wpfWindow.Show();
    }

    public virtual bool? ShowDialog()
    {
        return this.wpfWindow.ShowDialog();
    }

    #endregion

    protected Window WpfWindow
    {
        get { return this.wpfWindow; }
    }

    private static void ConfigureBehavior(ContentWindow cw)
    {
        cw.WindowStartupLocation = WindowStartupLocation.CenterOwner;
        cw.CommandBindings.Add(new CommandBinding(PresentationCommands.Accept, (sender, e) => cw.DialogResult = true));
    }
}

public static class PresentationCommands
{
    private readonly static RoutedCommand accept = new RoutedCommand("Accept", typeof(PresentationCommands));

    public static RoutedCommand Accept
    {
        get { return PresentationCommands.accept; }
    }
}

Then we have a specialized window adapter for the MainWindow which ensures the DataContext property is initialized correctly with the ViewModel.

internal class MainWindowAdapter : WindowAdapter
{
    private readonly ITrackerViewModelFactory vmFactory;
    private bool initialized;

    public MainWindowAdapter(Window wpfWindow, ITrackerViewModelFactory viewModelFactory)
        : base(wpfWindow)
    {
        if (viewModelFactory == null)
        {
            throw new ArgumentNullException("viewModelFactory");
        }

        this.vmFactory = viewModelFactory;
    }

    #region IWindow Members

    public override void Close()
    {
        this.EnsureInitialized();
        base.Close();
    }

    public override IWindow CreateChild(object viewModel)
    {
        this.EnsureInitialized();
        return base.CreateChild(viewModel);
    }

    public override void Show()
    {
        this.EnsureInitialized();
        base.Show();
    }

    public override bool? ShowDialog()
    {
        this.EnsureInitialized();
        return base.ShowDialog();
    }

    #endregion

    private void DeclareKeyBindings(TrackerViewModel vm)
    {
        //this.WpfWindow.InputBindings.Add(new KeyBinding(vm.RefreshCommand, new KeyGesture(Key.F5)));
        //this.WpfWindow.InputBindings.Add(new KeyBinding(vm.InsertProductCommand, new KeyGesture(Key.Insert)));
        //this.WpfWindow.InputBindings.Add(new KeyBinding(vm.EditProductCommand, new KeyGesture(Key.Enter)));
        //this.WpfWindow.InputBindings.Add(new KeyBinding(vm.DeleteProductCommand, new KeyGesture(Key.Delete)));
    }

    private void EnsureInitialized()
    {
        if (this.initialized)
        {
            return;
        }

        var vm = this.vmFactory.Create(this);
        this.WpfWindow.DataContext = vm;
        this.DeclareKeyBindings(vm);

        this.initialized = true;
    }
}

The Composition Root

And finally, you need a way to create the object graph. You are doing that in the correct place, but you are not doing yourself any favors by breaking it into many steps. And putting the container as an application-level variable is not necessarily a good thing - it opens up the container for abuse as a service locator.

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        // Begin Composition Root
        var container = new StandardKernel();

        // Register types
        container.Bind<ISystemEvents>().To<MySystemEvents>();
        container.Bind<ITrackerViewModelFactory>().To<TrackerViewModelFactory>();
        container.Bind<Window>().To<MainWindow>();
        container.Bind<IWindow>().To<MainWindowAdapter>();

        // Build the application object graph
        var window = container.Get<IWindow>();

        // Show the main window.
        window.Show();

        // End Composition Root
    }
}

I think the main issue you are having is that you need to ensure to call Show() on the MainWindow manually.

If you really do want to break the registration out into another step, you can do so by using one or more Ninject Modules.

using Ninject.Modules;
using System.Windows;

public class MyApplicationModule : NinjectModule
{
    public override void Load()
    {
        Bind<ISystemEvents>().To<MySystemEvents>();
        Bind<ITrackerViewModelFactory>().To<TrackerViewModelFactory>();
        Bind<Window>().To<MainWindow>();
        Bind<IWindow>().To<MainWindowAdapter>();
    }
}

And then the App.xaml.cs file will look like this:

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        // Begin Composition Root
        new StandardKernel(new MyApplicationModule()).Get<IWindow>().Show();

        // End Composition Root
    }
}
Kattiekatuscha answered 13/9, 2015 at 17:51 Comment(2)
One small problem, the application is not exiting on closing the window. Is there any natural way of exiting the application with this approach?Divot
PSA: In .net 4.5, there is a bug where you might need to add an attribute to the Application (App.xaml) otherwise the resources might not get loaded. As a workaround suggests, you can just set the Name and it works. connect.microsoft.com/VisualStudio/feedback/details/472729/…Encyst
F
0

The trackerviewmodel will be instantiated by the auto-generated xaml designer code, not by ninject. I've never used ninject, but I think you need to configure the container to know about your viewModel, and then inject the viewmodel for Ninject to resolve it and it's dependencies:

public class MainWindow : Window
{
    [Inject]
    public TrackerViewModel ViewModel { get; set; }

    public MainWindow()
    {
        InitializeComponent();
        DataContext = ViewModel;
    }
}
Fineberg answered 13/9, 2015 at 11:19 Comment(1)
This throws null reference exception on firing of the OnPropertyChanged event from viewmodel.Divot

© 2022 - 2024 — McMap. All rights reserved.