Home > other >  Custom Window with nested Generics produces NullReferencEException without Inner Exception
Custom Window with nested Generics produces NullReferencEException without Inner Exception

Time:05-10

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
}

CodePudding user response:

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.

  • Related