nb5t3uv
nb5t3uv

Reputation: 37

Null Reference Exception Adding Class Instance from Custom User Control to List In MainWindow

I'm working on a C# WPF project in Visual Studio in which I allow the user to add a user control (with two text boxes in the code below), which creates a class (with data stored within the user control so that multiple user controls can be created with a single button) and adds it to a list stored in the main window file to be written to a csv file later. I'm getting the following error when I hit the button to create a new user control:

An unhandled exception of type 'System.NullReferenceException' occurred in MasterListErrorTest.exe

Additional information: Object reference not set to an instance of an object.

I created a simplified version of my project that contains just the elements needed to reproduce the error. Here's all my code so you can plug it straight in and get the error for yourself. What am I doing wrong?

MainWindow.xaml:

<Window x:Class="MasterListErrorTest.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:MasterListErrorTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <StackPanel x:Name="UserControlContainer" HorizontalAlignment="Left" Height="216" Margin="24,83,0,0" VerticalAlignment="Top" Width="471" Orientation="Horizontal"/>
        <Button x:Name="CreateNewControl" Content="Create New" HorizontalAlignment="Left" Margin="76,37,0,0" VerticalAlignment="Top" Width="75" Click="CreateNewControl_Click"/>
        <Button x:Name="GiveStringFromList" Content="Give String" HorizontalAlignment="Left" Margin="360,37,0,0" VerticalAlignment="Top" Width="75" Click="GiveStringFromList_Click"/>

    </Grid>
</Window>

MainWindow.xaml.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace MasterListErrorTest
{
    public partial class MainWindow : Window
    {
        int MultipleTextBoxControlID = 1;

        public MainWindow()
        {
            InitializeComponent();
        }

        public static class TextBoxControlList
        {
            public static List <MultipleTextBoxControl.TextBoxData> MasterDataList;
        }

        private void CreateNewControl_Click(object sender, RoutedEventArgs e)
        {
            MultipleTextBoxControl newUserControl = new MultipleTextBoxControl(MultipleTextBoxControlID);
            UserControlContainer.Children.Add(newUserControl);
        }

        private void GiveStringFromList_Click(object sender, RoutedEventArgs e)
        {
            foreach (MultipleTextBoxControl.TextBoxData textBoxPanel in TextBoxControlList.MasterDataList)
            {
                List<string> userControlLine = new List<string>();

                userControlLine.Add(textBoxPanel.Identifier.ToString());
                userControlLine.Add(textBoxPanel.TextBox1Data);
                userControlLine.Add(textBoxPanel.TextBox2Data);

                MessageBox.Show(string.Join(",", userControlLine));
            }
        }
    }
}

MultipleTextBoxControl.xaml:

<UserControl x:Class="MasterListErrorTest.MultipleTextBoxControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:MasterListErrorTest"
             mc:Ignorable="d" 
             d:DesignHeight="50" d:DesignWidth="300">
    <Grid>
        <TextBox x:Name="textBox1" HorizontalAlignment="Left" Height="23" Margin="0,10,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="120" TextChanged="textBox1_TextChanged"/>
        <TextBox x:Name="textBox2" HorizontalAlignment="Left" Height="23" Margin="153,10,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="120" TextChanged="textBox2_TextChanged"/>

    </Grid>
</UserControl>

MultipleTextBoxControl.xaml.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace MasterListErrorTest
{
    public partial class MultipleTextBoxControl : UserControl
    {
        TextBoxData newTextBoxGroup = new TextBoxData();

        public MultipleTextBoxControl(int identifier)
        {
            InitializeComponent();

            newTextBoxGroup.Identifier = identifier;
            MainWindow.TextBoxControlList.MasterDataList.Add(newTextBoxGroup);
        }

        public class TextBoxData
        {
            public int Identifier { get; set; }
            public string TextBox1Data { get; set; }
            public string TextBox2Data { get; set; }

            public TextBoxData()
            {
                TextBox1Data = "Unchanged Textbox 1";
                TextBox2Data = "Unchanged Textbox 2";
            }
        }

        private void textBox1_TextChanged(object sender, TextChangedEventArgs e)
        {
            newTextBoxGroup.TextBox1Data = textBox1.Text;
        }

        private void textBox2_TextChanged(object sender, TextChangedEventArgs e)
        {
            newTextBoxGroup.TextBox2Data = textBox2.Text;
        }
    }
}

Upvotes: 1

Views: 1565

Answers (3)

Danil
Danil

Reputation: 895

I used semiglobal reference to MainWindow in user control like this:

public partial class MainWindow : Window
{
        public static MainWindow Me = null;
        public MainWindow()
        {
            Me = this;
            InitializeComponent();
            

Then I used this "Me" in User control:

 public partial class UserControlTable : UserControl
 {
      DataCenterSender m_DataCenterSender = new DataCenterSender(MainWindow.Me.Get_DataCenter());

And had a Null Pointer in XAML Editor.

Looks like constructor of MainWindow has not been called in XAML editor and since MainWindow.Me==null in this case User Control ctor failed.

Upvotes: 0

Mark Feldman
Mark Feldman

Reputation: 16128

If you put a try/catch exception block around the code in your CreateNewControl_Click then the caught exception will give you more information about what's going on. The StackTrace says this:

at GuiTest.MultipleTextBoxControl..ctor(Int32 identifier) in c:\Dev\GuiTest\MultipleTextBoxControl.xaml.cs:line 31
at GuiTest.MainWindow25.CreateNewControl_Click(Object sender, RoutedEventArgs e) in c:\Dev\GuiTest\MainWindow25.xaml.cs:line 43

So the most recent item on the list is MultipleTextBoxControl.xaml.cs line 31:

MainWindow25.TextBoxControlList.MasterDataList.Add(newTextBoxGroup);

Placing a breakpoint here and examining the contents reveals that MasterDataList is null, because you're not initializing it in TextBoxControlList:

public static List <MultipleTextBoxControl.TextBoxData> MasterDataList;

So do so:

public static List <MultipleTextBoxControl.TextBoxData> MasterDataList = new List<MultipleTextBoxControl.TextBoxData>();

Personally I would strongly advise against the use of static classes members, especially in cases like this, but either way this is the answer to your question.

EDIT:

I totally agree with the main points that AnjumSKhan is making, although I personally would go about it using Inversion of Control (IoC). What you're really trying to do is get the child controls to register their data in a way that the Main Window code can access later. As AnjumSKhan points out the children shouldn't know anything about their parent, but you should also be able to create and unit test this behaviour in your child class without needing to create a parent. Inversion of control involves passing in an interface to the child that it should use to register itself, a very simple example might be this:

public interface IDataRegistrationService
{
    void Register(MultipleTextBoxControl.TextBoxData data);
}

Your child window can accept a reference to such a service in its constructor and do the registration as it was before but using this service instead:

    public MultipleTextBoxControl(int identifier, IDataRegistrationService service)
    {
        InitializeComponent();
        newTextBoxGroup.Identifier = identifier;
        service.Register(newTextBoxGroup); // <---------
    }

Your MainWindow class can now inherit from this and pass a reference to itself in when the child is created (note I've also made MasterDataList a regular property of MainWindow):

    public static List <MultipleTextBoxControl.TextBoxData> MasterDataList = new List<MultipleTextBoxControl.TextBoxData>();

    public void Register(MultipleTextBoxControl.TextBoxData data)
    {
        MasterDataList.Add(data);
    }

    private void CreateNewControl_Click(object sender, RoutedEventArgs e)
    {
        MultipleTextBoxControl newUserControl = new MultipleTextBoxControl(MultipleTextBoxControlID, this);
        UserControlContainer.Children.Add(newUserControl);
    }

By doing this you've formalized the relationship between the parent and child and prevented yourself or others adding more relationships between the two that might be difficult to untangle or change later. You're also now in a position where you can create an instance of your child and use a mocked object (e.g. using the Moq library) to test that it behaves as expected. And by maintaining good separation of concerns you have the freedom to pass in any service you want...maybe later on you'll decide that you need multiple panels with one server per panel.

The one downside to IoC is that you wind up passing references to service provider all over your project, with middle layers keeping references to objects higher up the hierarchy for the sole purpose of passing them lower down. This is what Dependency Injection frameworks solve (e.g. Ninject). They get rid of all that parameter passing and your final code winds up looking something like this:

public partial class MultipleTextBoxControl : UserControl
{
    // this gets created by the DI framework, with identifier set automatically
    [Inject] private TextBoxData newTextBoxGroup { get; set; }

    // this get injected automatically when the class is create
    [Inject] private IDataRegistrationService DataService {get; set;}

    public MultipleTextBoxControl()
    {
        InitializeComponent();
    }

    // this gets called immediately after the constructor
    public void Initialize()
    {
        // and you do any custom initialization here, using injected components
        this.DataService.Register(newTextBoxGroup);
    }

Upvotes: 1

AnjumSKhan
AnjumSKhan

Reputation: 9827

Change your TextBoxControlList class to :

        public static class TextBoxControlList
        {
            public static List <MultipleTextBoxControl.TextBoxData> MasterDataList;
            static TextBoxControlList() {
                MasterDataList = new List<MultipleTextBoxControl.TextBoxData>();
            }
        }

A better way to do :

Refactor#1

MultipleTextBoxControl.xaml.cs

public partial class MultipleTextBoxControl : UserControl
    {
        TextBoxData _newTextBoxGroup;
        public TextBoxData TextBoxGroup { get { return _newTextBoxGroup; } }

        public MultipleTextBoxControl(int identifier)
        {
            InitializeComponent();

            _newTextBoxGroup = new TextBoxData(identifier);            

        }

        public class TextBoxData
        {
            public int Identifier { get; set; }
            public string TextBox1Data { get; set; }
            public string TextBox2Data { get; set; }

            public TextBoxData(int identifier)
            {
                Identifier = identifier;

                TextBox1Data = "Unchanged Textbox 1";
                TextBox2Data = "Unchanged Textbox 2";
            }
        }

        private void textBox1_TextChanged(object sender, TextChangedEventArgs e)
        {
            _newTextBoxGroup.TextBox1Data = textBox1.Text;
        }

        private void textBox2_TextChanged(object sender, TextChangedEventArgs e)
        {
            _newTextBoxGroup.TextBox2Data = textBox2.Text;
        }
    }

MainWindow.cs

public partial class MainWindow : Window
    {
        int MultipleTextBoxControlID = 1;
        public static List<MultipleTextBoxControl.TextBoxData> MasterDataList;

        static MainWindow() {
            MasterDataList = new List<MultipleTextBoxControl.TextBoxData>();
        }

        public MainWindow()
        {
            InitializeComponent();
        }


        private void CreateNewControl_Click(object sender, RoutedEventArgs e)
        {
            MultipleTextBoxControl newUserControl = new MultipleTextBoxControl(MultipleTextBoxControlID);
            UserControlContainer.Children.Add(newUserControl);

            MasterDataList.Add(newUserControl.TextBoxGroup);
        }

        private void GiveStringFromList_Click(object sender, RoutedEventArgs e)
        {
            foreach (MultipleTextBoxControl.TextBoxData textBoxPanel in MasterDataList)
            {
                List<string> userControlLine = new List<string>();

                userControlLine.Add(textBoxPanel.Identifier.ToString());
                userControlLine.Add(textBoxPanel.TextBox1Data);
                userControlLine.Add(textBoxPanel.TextBox2Data);

                MessageBox.Show(string.Join(",", userControlLine));
            }
        }
    }

Upvotes: 1

Related Questions