Solved

String array as a class property

Posted on 2014-10-07
25
216 Views
Last Modified: 2014-10-16
In VB 2008, I have a class that I use to store options for my application. The class is serialized to an .xml file to keep the options from one run to the next. I have a list of titles which has been 4 in length, so I simply showed them in the class definition like so:
<Serializable()> _
Public Class Options
    ' System options
    Public blnDisplayExcel As Boolean
    Public strUserCategory1 As String       
    Public strUserCategory2 As String       
    Public strUserCategory3 As String       
    Public strUserCategory4 As String       
End Class

Open in new window

There are also methods to save to a .xml file and retrieve from the .xml file. (Thanks, RonaldBiemans, for the original code.)

Now, I'm expanding the number of categories to 20, and it's going to be much more efficient to handle them as an array rather than 20 separately named properties. But I'm not sure how to accomplish that. If I just define a property as
Public strUserCategory(20) As String
I get "Object reference not set to an instance of an object" when I try to set strUserCategory(1)="XYZ".

I tried restructuring it as:
    <System.Runtime.Serialization.OptionalField()> Private pstrUserCategory(20) As String
    Public Property strUserCategory(ByVal i As Integer) As String
        Get
            If i < 1 Or i > 20 Then
                Throw New Exception("Out of range 1-20")
            Else
                Return pstrUserCategory(i)
            End If
        End Get
        Set(ByVal value As String)
            pstrUserCategory(i) = value
        End Set
    End Property

Open in new window

(The OptionalField attribute is there so deserializing the class with a new option doesn't generate an exception.) This gives the same "Object reference not set..." exception when I execute the Set code.

I'm not sure how I'm supposed to initialize the array. This feels like it should be simple, but somehow I'm not seeing it. FWIW, there is currently no New method for the class; should I be doing something there?
0
Comment
Question by:ElrondCT
  • 10
  • 8
  • 5
  • +1
25 Comments
 
LVL 32

Expert Comment

by:it_saige
ID: 40367286
There should be no reason why you could not use a list:
Imports System.Collections.Generic

<Serializable()> _
Public Class Options
    ' System options
    Public blnDisplayExcel As Boolean
    Public listCategories As List(Of String) = New List(Of String)
End Class

Open in new window


Or a Dictionary (if order matters):
Imports System.Collections.Generic

<Serializable()> _
Public Class Options
    ' System options
    Public blnDisplayExcel As Boolean
    Public dictCategories As Dictionary(Of Int, String) = New Dictionary(Of Int, String)
End Class

Open in new window


In either case, once you instansiate your Options, you would reference the properties directly:
Imports System.Collections.Generic

<Serializable()> _
Public Class Options
	Private fDisplayExcel As Boolean = False
	Private fCategories As List(Of String) = New List(Of String)

	Public Property DisplayExcel() As Boolean
		Get
			Return fDisplayExcel
		End Get
		Set(ByVal value As Boolean)
			If Not value.Equals(fDisplayExcel) Then
				fDisplayExcel = value
			End If
		End Set
	End Property

	Public Property Categories() As List(Of String)
		Get
			Return fCategories
		End Get
		Set(ByVal value As List(Of String))
			If Not value.Equals(fCategories) Then
				fCategories = value
			End If
		End Set
	End Property
End Class

Public Class Form1
	Dim options As Options

	Private Sub Form1_Load(ByVal sender As Object, ByVal e As EventArgs) Handles Me.Load
		options.DisplayExcel = True
		options.Categories.Add("Category1Option")
		If (options.DisplayExcel) Then
			For Each category In options.Categories
				MessageBox.Show(category)
			Next
		End If
	End Sub
End Class

Open in new window


-saige-
0
 
LVL 20

Author Comment

by:ElrondCT
ID: 40367622
Thanks for the suggestion, but I don't think this is going to work. I don't want to be passing the entire set of strings when I reference them. I just want one particular item of the set. So I'd like to be able to say (using an instantiated class, of course):

Options.strUserCategory(3) = "ABC"
txt.Text = Options.strUserCategory(4)

etc.

When I try using the List class, VB tells me "Value of type 'System.Collections.Generic.List(Of String)' cannot be converted to 'String.'
0
 
LVL 74

Expert Comment

by:käµfm³d 👽
ID: 40368065
Can you provide a sample of the XML (structure)?
0
 
LVL 32

Expert Comment

by:it_saige
ID: 40368146
The error means that you are attempting to assign the list to a string variable as opposed to a list item to the string variable.  There are two ways you would access the items of the list to assign an item or items to a string variable.

1.  Enumerate over the list (multiple ways to accomplish this); i.e. - For, While, For Each, Linq, etc.
2.  Indexer access; i.e. List(i).

There are many similarities between a list and an array.  But there are also many differences.  The main one here that will come into play is initialization.  In an array when you stipulate foo(20), you initialize a contiguous structure in memory that represents 20 foo's.  In a list however, the memory structure is dynamically managed as you add and remove items to and from the list.  This is because a list represents a (compared to an array at least) loosely bound collection of references.

The initialization factor is also why I mentioned a dictionary.  The difference between a dictionary and a list is that a dictionary is a collection of KeyValuePairs.  However, a dictionary does not include an indexer whereas a list does.  This is because order does not matter in a dictionary.  Order does not matter because your key (which would be the indexer) could be any type.

-saige-
0
 
LVL 32

Expert Comment

by:it_saige
ID: 40368284
Here is an example implementation that I quickly whipped up using a string array:
<Serializable()> _
Public Class Options
	Private fDisplayExcel As Boolean = False
	Private fCategories(20) As String
	Public Property DisplayExcel() As Boolean
		Get
			Return fDisplayExcel
		End Get
		Set(ByVal value As Boolean)
			If Not value.Equals(fDisplayExcel) Then
				fDisplayExcel = value
			End If
		End Set
	End Property

	Public Property Categories(ByVal index As Integer) As String
		Get
			If index < 1 Or index > 20 Then
				Throw New Exception("Out of range 1-20")
			Else
				Return IIf(fCategories(index) <> Nothing, fCategories(index), String.Format("Category {0} - is empty.", index))
			End If
		End Get
		Set(ByVal value As String)
			If Not value.Equals(fCategories(index)) Then
				fCategories(index) = value
			End If
		End Set
	End Property
End Class

Module Module1
	Sub Main()
		Dim options As New Options
		options.Categories(3) = "ABC"
		options.Categories(4) = "DEF"
		options.Categories(20) = "XYZ"
		For i As Integer = 1 To 20
			Console.WriteLine(options.Categories(i))
		Next
		Console.ReadLine()
	End Sub
End Module

Open in new window


Produces the following output:Capture.JPG
-saige-
0
 
LVL 40

Assisted Solution

by:Jacques Bourgeois (James Burger)
Jacques Bourgeois (James Burger) earned 200 total points
ID: 40369018
Contrary to other basic types such as Integer and Date, the String is initialized with a value of Nothing. It has no value until you assign one, thus your NullReference exception.

Add a constructor that initializes your array with empty strings, so they are initialized and never return a NullReference unless you explicitely set them to Nothing later.

      Public Sub New()
            For x As Integer = 0 To 20
                  strUserCategory(x) = String.Empty
            Next
      End Sub
0
 
LVL 20

Author Comment

by:ElrondCT
ID: 40373557
JamesBurger, your code would work perfectly except for one thing: I'm using <System.Runtime.Serialization.OptionalField()> as an attribute on

Private pstrUserCategory(20) As String

Without the attribute, it's fine. But I need that to allow me to load an existing serialized .xml file which may not have this new array in it. And when I put the attribute on, even if I set up the New constructor like you describe (and the debugger shows me that it's getting called), I get an "Object reference not set..." exception when I execute the Set code of the strUserCategory(n) property.

If there's another way to load the old-style serialized class and update, that would be fine with me. It occurs to me that back in the VB 2003 days, before the OptionalField attribute, I had to manually parse the .xml file to update the class structure, and I suppose I could dust off that code. But that's a pretty tedious way to solve it...
0
 
LVL 32

Expert Comment

by:it_saige
ID: 40373583
Did you attempt using the string array implementation I whipped up?  Or does it still give you an Object reference exception?

-saige-
0
 
LVL 74

Expert Comment

by:käµfm³d 👽
ID: 40373767
Was my request (http:#answer40368065) overlooked, or is it not able to be satisfied?
0
 
LVL 20

Author Comment

by:ElrondCT
ID: 40373788
Saige, I'm using essentially the same property code as you describe:
    <System.Runtime.Serialization.OptionalField()> Private pstrUserCategory(20) As String
    Public Property strUserCategory(ByVal i As Integer) As String
        Get
            If i < 1 Or i > 20 Then
                Throw New Exception("Out of range 1-20")
            Else
                Return pstrUserCategory(i)
            End If
        End Get
        Set(ByVal value As String)
            pstrUserCategory(i) = value
        End Set
    End Property

Open in new window

I also have JamesBurger's suggested constructor code. When the New method runs, initializing all the elements of pstrUserCategory() works fine. When I set a property using a call like
   opt.strUserCategory(1) = "Something"
I get the Object reference exception on the line: pstrUserCategory(i) = value
0
 
LVL 32

Expert Comment

by:it_saige
ID: 40373894
I only ask because I added the OptionalField attribute to the private variable and still compiled and ran the program I used in my above example with the same results.

My next question is are you declaring a New Options class, e.g.
Dim options as New Options

Open in new window

vs.
Dim options as Options

Open in new window

If you are not, this indicates why just adding the constructor as recommended by James did not work either.  If you don't specify New then you are not calling the constructor.

So how can you make it work without calling the constructor, you need to change your class into a module:
<Serializable()> _
Public Module Options
	Private fDisplayExcel As Boolean = False
	<System.Runtime.Serialization.OptionalField()> _
	Private fCategories(20) As String
	Public Property DisplayExcel() As Boolean
		Get
			Return fDisplayExcel
		End Get
		Set(ByVal value As Boolean)
			If Not value.Equals(fDisplayExcel) Then
				fDisplayExcel = value
			End If
		End Set
	End Property

	Public Property Categories(ByVal index As Integer) As String
		Get
			If index < 1 Or index > 20 Then
				Throw New Exception("Out of range 1-20")
			Else
				Return IIf(fCategories(index) <> Nothing, fCategories(index), String.Format("Category {0} - is empty.", index))
			End If
		End Get
		Set(ByVal value As String)
			If Not value.Equals(fCategories(index)) Then
				fCategories(index) = value
			End If
		End Set
	End Property
End Module

Module Module1
	Sub Main()
		Options.Categories(3) = "ABC"
		options.Categories(4) = "DEF"
		options.Categories(20) = "XYZ"
		For i As Integer = 1 To 20
			Console.WriteLine(options.Categories(i))
		Next
		Console.ReadLine()
	End Sub
End Module

Open in new window


Produces the following output:Capture.JPG-saige-
0
 
LVL 40
ID: 40373895
If my constructor code does not work, then you added or remove something. I just checked the following with Dim x As New Class2, and it works perfectly:

Public Class Class2

	Private strUserCategory(20) As String

	Public Sub New()
		For x As Integer = 0 To 20
			'strUserCategory(x) = "Something"
			strUserCategory(x) = String.Empty
		Next
	End Sub

End Class

Open in new window


What is the exact exception that you get? There is nothing like an ObjectReferenceException in the documentation of the Framework.
0
6 Surprising Benefits of Threat Intelligence

All sorts of threat intelligence is available on the web. Intelligence you can learn from, and use to anticipate and prepare for future attacks.

 
LVL 40
ID: 40373908
Think I've got it. Is your variable still name pstrUserCategory, with a p. Mine is strUserCategory, without the p.

Sorry, I did not see the p, which usually means a pointer, and is usually not used in VB. The standard, for a property and its private variable is and underscore  :

Private _UserCategory As String

Public Property UserCategory(ByVal index As Integer) As String
0
 
LVL 32

Expert Comment

by:it_saige
ID: 40373947
@James - believe this is what he is referring to:
<Serializable()> _
Public Class Options
	Private fDisplayExcel As Boolean = False
	<System.Runtime.Serialization.OptionalField()> _
	Private fCategories(20) As String
	Public Property DisplayExcel() As Boolean
		Get
			Return fDisplayExcel
		End Get
		Set(ByVal value As Boolean)
			If Not value.Equals(fDisplayExcel) Then
				fDisplayExcel = value
			End If
		End Set
	End Property

	Public Property Categories(ByVal index As Integer) As String
		Get
			If index < 1 Or index > 20 Then
				Throw New Exception("Out of range 1-20")
			Else
				Return IIf(fCategories(index) <> Nothing, fCategories(index), String.Format("Category {0} - is empty.", index))
			End If
		End Get
		Set(ByVal value As String)
			If Not value.Equals(fCategories(index)) Then
				fCategories(index) = value
			End If
		End Set
	End Property

	Public Sub New()
		For x As Integer = 0 To 20
			fCategories(x) = String.Empty
		Next
	End Sub
End Class

Module Module1
	Sub Main()
		Dim options As Options
		options.Categories(3) = "ABC"
		options.Categories(4) = "DEF"
		options.Categories(20) = "XYZ"
		For i As Integer = 1 To 20
			Console.WriteLine(options.Categories(i))
		Next
		Console.ReadLine()
	End Sub
End Module

Open in new window


Produces the following exception:Capture.JPG
0
 
LVL 20

Author Comment

by:ElrondCT
ID: 40374044
Yikes, I need to respond to a lot of things. First, kaufmed:

My apologies for not responding sooner to your note. The real problem comes when there's nothing in the XML for the string array, so I'm not sure how helpful the XML is, but here's a piece of it (editing out a bunch of properties that are irrelevant):
<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:clr="http://schemas.microsoft.com/soap/encoding/clr/1.0" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<a1:Options id="ref-1" xmlns:a1="http://schemas.microsoft.com/clr/nsassem/EZ13/EZ13%2C%20Version%3D4.6.9.30374%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">
<strUserCategory1 id="ref-4">Division</strUserCategory1>
<strUserCategory2 id="ref-5">State</strUserCategory2>
<strUserCategory3 href="#ref-3"/>
<strUserCategory4 href="#ref-3"/>
<intCommitYears>5</intCommitYears>
<blnComboboxUDF>false</blnComboboxUDF>
</a1:Options>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Open in new window

When I create an XML file that includes the new array, it looks like:
<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:clr="http://schemas.microsoft.com/soap/encoding/clr/1.0" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<a1:Options id="ref-1" xmlns:a1="http://schemas.microsoft.com/clr/nsassem/EZ13/EZ13%2C%20Version%3D4.6.9.40835%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">
<intCommitYears>5</intCommitYears>
<pblnComboboxUDF href="#ref-7"/>
<pstrUserCategory href="#ref-8"/>
</a1:Options>
<SOAP-ENC:Array id="ref-7" SOAP-ENC:arrayType="xsd:boolean[21]">
<item>False</item>
<item>False</item>
<item>False</item>
<item>False</item>
<item>False</item>
<item>False</item>
<item>False</item>
<item>False</item>
<item>False</item>
<item>False</item>
<item>False</item>
<item>False</item>
<item>False</item>
<item>False</item>
<item>False</item>
<item>False</item>
<item>False</item>
<item>False</item>
<item>False</item>
<item>False</item>
<item>False</item>
</SOAP-ENC:Array>
<SOAP-ENC:Array id="ref-8" SOAP-ENC:arrayType="xsd:string[21]">
<item href="#ref-3"/>
<item href="#ref-3"/>
<item href="#ref-3"/>
<item href="#ref-3"/>
<item href="#ref-3"/>
<item href="#ref-3"/>
<item href="#ref-3"/>
<item href="#ref-3"/>
<item href="#ref-3"/>
<item href="#ref-3"/>
<item href="#ref-3"/>
<item href="#ref-3"/>
<item href="#ref-3"/>
<item href="#ref-3"/>
<item href="#ref-3"/>
<item href="#ref-3"/>
<item href="#ref-3"/>
<item href="#ref-3"/>
<item href="#ref-3"/>
<item href="#ref-3"/>
<item href="#ref-3"/>
</SOAP-ENC:Array>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Open in new window

My challenge at the moment is how to get from #1 to #2.
0
 
LVL 20

Author Comment

by:ElrondCT
ID: 40374093
Saige & James:

I'm definitely instantiating the class with

opt = New Options

before calling the load method of the class:

opt = Options.LoadXML(gstrOptionsFolder & "\Options.xml")

If I step through in the debugger, I can see the New constructor executing. And in it,

 pstrUserCategory(i) = ""

executes with no problems, but seemingly the same code in the property method crashes.

Some more testing has given me more information. The problem is coming from loading the XML file. I have the code

            fStream = New System.IO.FileStream(Path, IO.FileMode.Open, IO.FileAccess.Read)
            tmp = CType(myFormat.Deserialize(fStream), Options)

Before this code, tmp.strUserCategory(1) = "". Immediately after executing this code, if the array doesn't previously exist, I get "Object reference..." So the question is, how do I instantiate the array when it's not coming from the XML file?
0
 
LVL 40
ID: 40374161
Deserializing the data in the Options replace the object that was created through the constructor with the object that was serialized. The constructor is thus useless in such a case.

If you are deserializing from the file that was created with an object that had no constructor, then this is what you get when you deserialize.

Serialization is a powerful tool, but it can lead to problems of that type when you deserialize a file in a class that was modified between serialization and deserialization.
0
 
LVL 20

Author Comment

by:ElrondCT
ID: 40374176
But the OptionalField attribute is supposed to resolve that problem. It works fine for non-arrays, and for arrays of non-strings.

Looks like maybe I need to process the XML as text to rebuild the class. I've got about 60 properties to process, which is tedious, but most of them I had when I wrote such an updater using VB 2003 (before OptionalField existed).
0
 
LVL 40
ID: 40374370
OptionalField prevents an exception if the property did not exist in the previous version. But if the property was there and was filled with an array that was not initialized, what you get back is an array that was not initialized.

What you could try is check if the array element contains something, and set it to an empty string if it is, something like:

If pstrUserCategory(i) Is Nothing Then
     pstrUserCategory(i)=String.Empty
End If
0
 
LVL 20

Author Comment

by:ElrondCT
ID: 40374437
I tried that and I'm getting the "Object reference" exception on my Is Nothing check. Even if I just use the assignment statement, I'm getting the exception, though I should mention that I have to set the value in an instantiated member (tmp.pstrUserCategory(i)=String.Empty) rather than in generic code like New() uses. The Deserialize method is called inside a shared function, so the variable references have to include a specific instance.

Sorry if I'm not communicating clearly; I need to get to bed.
0
 
LVL 20

Accepted Solution

by:
ElrondCT earned 0 total points
ID: 40375281
I found a solution. Rather than defining the number of elements in the initial Dim statement, I changed the Dim to

   <System.Runtime.Serialization.OptionalField()> Private _strUserCategory() As String

and then put a ReDim in the constructor. I then added a check after loading the XML file:

        Try
            Dim strTest As String = _strUserCategory(0)
        Catch ex As Exception
            ReDim _strUserCategory(20)
            For i As Integer = 0 To 20
                _strUserCategory(i) = ""
            Next
            SaveXml(Path)
        End Try

The ReDim seems to work properly to instantiate the string array, and I'm off and running.

The only thing I don't like about this is that it relies on an exception, which of course is a slow process. I'd be much happier if I could catch the situation of "Object reference not set to an instance of the object" with a boolean test rather than an exception, but I haven't been able to figure out a way to do that. _strUserCategory(1)  Is Nothing gives the same error. But since this update is a one-time event, it's not something I'm going to spend more time on, unless someone has a good idea for an alternative.
0
 
LVL 32

Assisted Solution

by:it_saige
it_saige earned 200 total points
ID: 40375300
What about checking if _strUserCategory Is Nothing as opposed to an index location?

-saige-
0
 
LVL 20

Author Comment

by:ElrondCT
ID: 40375305
Thanks, saige. You're right.

Thanks, all, for your help on thinking this through.
0
 
LVL 32

Expert Comment

by:it_saige
ID: 40375306
Not a problem.  Happy coding.

-saige-
0
 
LVL 20

Author Closing Comment

by:ElrondCT
ID: 40383882
My comment selected as solution, because I found it independently of suggestions made; other assistance credited.
0

Featured Post

Better Security Awareness With Threat Intelligence

See how one of the leading financial services organizations uses Recorded Future as part of a holistic threat intelligence program to promote security awareness and proactively and efficiently identify threats.

Join & Write a Comment

Suggested Solutions

Creating an analog clock UserControl seems fairly straight forward.  It is, after all, essentially just a circle with several lines in it!  Two common approaches for rendering an analog clock typically involve either manually calculating points with…
If you need to start windows update installation remotely or as a scheduled task you will find this very helpful.
This video shows how to remove a single email address from the Outlook 2010 Auto Suggestion memory. NOTE: For Outlook 2016 and 2013 perform the exact same steps. Open a new email: Click the New email button in Outlook. Start typing the address: …
This tutorial demonstrates a quick way of adding group price to multiple Magento products.

744 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question

Need Help in Real-Time?

Connect with top rated Experts

11 Experts available now in Live!

Get 1:1 Help Now