'Custom Window with nested Generics produces NullReferencEException without Inner Exception

I have created a coupled Window <-> Controller (what I prefer to call my ViewModels) class relationship with the intent of handling a lot of boilerplate code that every window and controller uses.

Unfortunately though, this seems to have caused some form of runtime issue. I believe the problem is some form of bad pathing for my window's DataContext, despite the fact I have no Bindings declared (yet) on the window...

public abstract class ModelledWindow<TWindow, TController> : Window
    where TWindow : ModelledWindow<TWindow, TController>
    where TController : WindowControllerBase<TWindow, TController>
{
    protected TController Controller { get; }
    public ModelledWindow(TController controller)
    {
        DataContext = Controller = controller ?? throw new ArgumentNullException(nameof(controller));
    }
}

public abstract class ControllerBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected T Mutate<T>(T value, [CallerMemberName] string name = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        return value;
    }
}

public abstract class WindowControllerBase<TWindow, TController> : ControllerBase
    where TWindow : ModelledWindow<TWindow, TController>
    where TController : WindowControllerBase<TWindow, TController>
{
    public ICommand ShowWindowCmd { get; }
    private Func<TWindow> WindowBuilder { get; }
    public WindowControllerBase(CommandFactory commandFactory, Func<TWindow> windowBuilder)
    {
        ShowWindowCmd = commandFactory.Compose(ShowWindow, CanShowWindow);
        WindowBuilder = windowBuilder ?? throw new ArgumentNullException(nameof(windowBuilder));
    }

    private TWindow _window;
    private void ShowWindow()
    {
        _window ??= WindowBuilder();
        _window.Show();
    }

    private Visibility Visibility => _window?.Visibility ?? Visibility.Hidden;
    private bool CanShowWindow() => Visibility != Visibility.Visible;

}

MainWindow.xaml:

<local:ModelledWindow
    x:Class="PoeAutoClipboard.Wpf.MainWindow"
    x:TypeArguments="local:MainWindow,local:MainWindowController"
    xmlns:local="clr-namespace:PoeAutoClipboard.Wpf"
    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"
    mc:Ignorable="d"
    Title="PoeAutoClipboard" 
    Height="46" 
    Width="155" 
>
    <Grid>
    </Grid>
</local:ModelledWindow>

MainWindow.xaml.cs:

public partial class MainWindow : ModelledWindow<MainWindow, MainWindowController>
{
    public MainWindow(MainWindowController controller) : base(controller)
    {
        InitializeComponent();
    }
}

public class MainWindowController : WindowControllerBase<MainWindow, MainWindowController>
{

    public MainWindowController(
        CommandFactory commandFactory,
        Func<MainWindow> windowBuilder
    ) : base(commandFactory, windowBuilder)
    {
    }

}

I automatically scaffold everything up via Dependency Injection in my App layer like so:

public partial class App : Application
{
    private const string ConfigurationPath = "AppSettings.json";
    private ConfigurationController ConfigController { get; set; }

    public App()
    {
        var config = new ConfigurationBuilder()
            .AddJsonFile(ConfigurationPath, true, true)
            .Build();

        ConfigController = config.Get<ConfigurationController>() ?? new ConfigurationController();
    }

    private void App_OnStartup(object sender, StartupEventArgs e)
    {
        var services = new ServiceCollection();
        ConfigureServices(services);

        var serviceProvider = services.BuildServiceProvider();

        var mainWindow = serviceProvider.GetRequiredService<MainWindowController>();
        mainWindow.ShowWindowCmd.Execute(null);
    }

    private void ConfigureServices(IServiceCollection services)
    {
        services.UseMVC<App>();
        
        // Remove the auto registered "blank" config and add our loaded config VM instead
        services.Remove(ServiceDescriptor.Singleton(typeof(ConfigurationController)));
        services.AddSingleton(ConfigController);

        // Services
        services.AddSingleton<CommandFactory>();
    }
}

When I inspect MainWindowController and MainWindow everything seems fine. The Controller is not null and the DataContext has been assigned the way I expected it to be. I can inspect MainWindow.DataContext and its as I expect to be (a non-null instance of MainWindowController)

Furthermore, MainWindow.g.i.cs appears to be working exactly as expected, its building as such:

public partial class MainWindow : PoeAutoClipboard.Wpf.ModelledWindow<PoeAutoClipboard.Wpf.MainWindow, PoeAutoClipboard.Wpf.MainWindowController>, System.Windows.Markup.IComponentConnector {
    ...
}

Everything compiles and runs fine, but then I hit a runtime exception here, in the Constructor of MainWindow:

private void App_OnStartup(object sender, StartupEventArgs e)
{
    var services = new ServiceCollection();
    ConfigureServices(services);

    var serviceProvider = services.BuildServiceProvider();

    var mainWindow = serviceProvider.GetRequiredService<MainWindowController>();
    mainWindow.ShowWindowCmd.Execute(null); <<<<<< Here (Calling method up stack)
}


public MainWindow(MainWindowController controller) : base(controller)
{
    InitializeComponent(); <<<<<< Here (Actual exception thrown here)
}

And the exception is... extremely unhelpful.

Message: System.NullReferenceException: 'Object reference not set to an instance of an object.'

Stack Trace:

 at System.Windows.Markup.WpfXamlLoader.TransformNodes(XamlReader xamlReader, XamlObjectWriter xamlWriter, Boolean onlyLoadOneNode, Boolean skipJournaledProperties, Boolean shouldPassLineNumberInfo, IXamlLineInfo xamlLineInfo, IXamlLineInfoConsumer xamlLineInfoConsumer, XamlContextStack`1 stack, IStyleConnector styleConnector)
 at System.Windows.Markup.WpfXamlLoader.Load(XamlReader xamlReader, IXamlObjectWriterFactory writerFactory, Boolean skipJournaledProperties, Object rootObject, XamlObjectWriterSettings settings, Uri baseUri)
 at System.Windows.Markup.WpfXamlLoader.LoadBaml(XamlReader xamlReader, Boolean skipJournaledProperties, Object rootObject, XamlAccessLevel accessLevel, Uri baseUri)
 at System.Windows.Markup.XamlReader.LoadBaml(Stream stream, ParserContext parserContext, Object parent, Boolean closeStream)
 at System.Windows.Application.LoadComponent(Object component, Uri resourceLocator)
 at PoeAutoClipboard.Wpf.MainWindow.InitializeComponent() in D:\Documents\Projects\Programming\CSharp\PoeAutoClipboard\PoeAutoClipboard.Wpf\MainWindow.xaml:line 1
 at PoeAutoClipboard.Wpf.MainWindow..ctor(MainWindowController controller) in D:\Documents\Projects\Programming\CSharp\PoeAutoClipboard\PoeAutoClipboard.Wpf\MainWindow.xaml.cs:line 14
 at System.RuntimeMethodHandle.InvokeMethod(Object target, Span`1& arguments, Signature sig, Boolean constructor, Boolean wrapExceptions)
 at System.Reflection.RuntimeConstructorInfo.Invoke(BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
 at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
 at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
 at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitDisposeCache(ServiceCallSite transientCallSite, RuntimeResolverContext context)
 at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
 at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
 at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass2_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
 at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
 at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
 at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
 at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
 at PoeAutoClipboard.Wpf.WindowControllerBase`2.ShowWindow() in D:\Documents\Projects\Programming\CSharp\PoeAutoClipboard\PoeAutoClipboard.Wpf\ControllerBase.cs:line 34
 at PoeAutoClipboard.Wpf.Extensions.CommandFactoryExtensions.<>c__DisplayClass4_0.<Compose>b__0(Object p) in D:\Documents\Projects\Programming\CSharp\PoeAutoClipboard\PoeAutoClipboard.Wpf\Extensions\CommandFactoryExtensions.cs:line 33
 at PoeAutoClipboard.Wpf.Command.Execute(Object parameter) in D:\Documents\Projects\Programming\CSharp\PoeAutoClipboard\PoeAutoClipboard.Wpf\Command.cs:line 23
 at PoeAutoClipboard.Wpf.App.App_OnStartup(Object sender, StartupEventArgs e) in D:\Documents\Projects\Programming\CSharp\PoeAutoClipboard\PoeAutoClipboard.Wpf\App.xaml.cs:line 38
 at System.Windows.Application.OnStartup(StartupEventArgs e)
 at System.Windows.Application.<.ctor>b__1_0(Object unused)
 at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
 at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
 at System.Windows.Threading.DispatcherOperation.InvokeImpl()
 at System.Windows.Threading.DispatcherOperation.InvokeInSecurityContext(Object state)
 at MS.Internal.CulturePreservingExecutionContext.CallbackWrapper(Object obj)
 at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)

No Inner Exception.

Anyone have insight on what I missed here? Also, if I change my code in App Startup to this, it throws the exact same exception:

private void App_OnStartup(object sender, StartupEventArgs e)
{
   var services = new ServiceCollection();
   ConfigureServices(services);

   var serviceProvider = services.BuildServiceProvider();

   var mainWindow = serviceProvider.GetRequiredService<MainWindow>();
   mainWindow.Show(); <<<<<< Breaks here, same exception, throws on same spot in MainWindow Constructor
}


Solution 1:[1]

The issue appears to be generics, I was able to solve this by simply manually adding a "secondary" intermediary base abstract class that concrete type'd the generics. I needed a second generic as well for project needs, but the end result looks like this:

ControlledWindowBase.cs :

public abstract class ControlledWindowBase<TWindow, TController> : Window
    where TWindow : ControlledWindowBase<TWindow, TController>
    where TController : WindowControllerBase<TWindow, TController>
{
    protected TController Controller { get; }
    public ControlledWindowBase(TController controller)
    {
        DataContext = Controller = controller ?? throw new ArgumentNullException(nameof(controller));
    }
}

WindowControllerBase.cs :

public abstract class WindowControllerBase<TWindow, TController> : ControllerBase
    where TWindow : ControlledWindowBase<TWindow, TController>
    where TController : WindowControllerBase<TWindow, TController>
{
    public ICommand ShowCmd { get; private set; }
    public ICommand HideCmd { get; private set; }
    private Func<TWindow> WindowFactory { get; }

    public WindowControllerBase(CommandFactory commandFactory, Func<TWindow> windowFactory)
    {
        ShowCmd = commandFactory.Compose(Show, CanShow);
        HideCmd = commandFactory.Compose(Hide, CanHide);
        WindowFactory = windowFactory ?? throw new ArgumentNullException(nameof(windowFactory));
    }

    private TWindow _window;
    private void Show()
    {
        _window ??= WindowFactory();
        _window.Show();
    }

    private bool CanShow()
    {
        return _window == null || _window.Visibility != System.Windows.Visibility.Visible;
    }

    private void Hide()
    {
        _window?.Hide();
    }

    private bool CanHide()
    {
        return _window != null && _window.Visibility == System.Windows.Visibility.Visible;
    }
}

MainWindow.xaml.cs :

public abstract class MainWindowBase : ControlledWindowBase<MainWindow, MainWindowController>
{
    public MainWindowBase(MainWindowController controller) : base(controller) { }
}

/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : MainWindowBase
{
    public MainWindow(MainWindowController controller) : base(controller)
    {
        InitializeComponent();
    }

    private void Window_OnDeactivated(object sender, EventArgs e)
    {
        Activate();
    }

    private void Window_OnMouseDown(object sender, MouseButtonEventArgs e)
    {
        if (e.ChangedButton == MouseButton.Left)
        {
            DragMove();
            Controller.ConfigController.InitialTop = Top;
            Controller.ConfigController.InitialLeft = Left;
        }
    }
}

public class MainWindowController : WindowControllerBase<MainWindow, MainWindowController>
{
    public ApplicationController ApplicationController { get; }
    public ConfigWindowController ConfigWindowController { get; }
    public ConfigurationController ConfigController { get; }

    public MainWindowController(
        CommandFactory commandFactory,
        Func<MainWindow> windowFactory,
        ApplicationController applicationController,
        ConfigWindowController configWindowController,
        ConfigurationController configController
    ) : base(commandFactory, windowFactory)
    {
        ApplicationController = applicationController ?? throw new ArgumentNullException(nameof(applicationController));
        ConfigWindowController = configWindowController ?? throw new ArgumentNullException(nameof(configWindowController));
        ConfigController = configController ?? throw new ArgumentNullException(nameof(configController));
    }

}

MainWindow.xaml :

<local:MainWindowBase
    x:Class="PoeAutoClipboard.Wpf.MainWindow"
    xmlns:local="clr-namespace:PoeAutoClipboard.Wpf"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:tb="http://www.hardcodet.net/taskbar" 
    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"
    mc:Ignorable="d"
    Background="Transparent"
    Title="PoeAutoClipboard" 
    Height="46" 
    Width="155" 
    WindowStyle="None" 
    AllowsTransparency="True" 
    Cursor="Hand" 
    ShowInTaskbar="False"
    Topmost="true"
    Icon="/Application.ico"
    MouseDown="Window_OnMouseDown"
    Deactivated="Window_OnDeactivated" 
>
    <Grid
        Background="Transparent"
        >
        <tb:TaskbarIcon
            IconSource="/Application.ico"
            ToolTipText="PoeAutoClipboard">
            <tb:TaskbarIcon.ContextMenu >
                <ContextMenu>
                    <MenuItem 
                        Header="Config" 
                        InputGestureText="Ctrl+F12"
                        Command="{Binding ConfigWindowController.ShowCmd}"
                    >
                        <MenuItem.Icon>
                            <Image Source="/Gear.ico"/>
                        </MenuItem.Icon>
                    </MenuItem>
                    <Separator/>
                    <MenuItem 
                        Header="Exit" 
                        Command="{Binding ApplicationController.ExitCmd}"
                    >
                        <MenuItem.Icon>
                            <Image Source="/X.ico"/>
                        </MenuItem.Icon>
                    </MenuItem>
                </ContextMenu>
            </tb:TaskbarIcon.ContextMenu>
        </tb:TaskbarIcon>
        <Label 
            Background="White" 
            BorderThickness="2" 
            BorderBrush="Black" 
            Content="Test" 
            HorizontalContentAlignment="Center" 
            HorizontalAlignment="Center" 
            VerticalAlignment="Center" 
            Width="93" 
        />
    </Grid>
</local:MainWindowBase>

Note a few things:

  1. The existence of MainWindowBase in MainWindow.xaml.cs which purely serves the purpose of concrete typing the generics

  2. As a result of their being no generics in the inheritence of MainWindow : MainWindowBase the xaml has no x:TypeArguments attribute needed.

This seems to be the "magic sauce" to get WPF to play nice with generics, a single one liner abstract class to "force" the types, and you just have to make one for each of your window's code behinds.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Steffen Cole Blake