ElrondCT
asked on
String array as a class property
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:
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:
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?
<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
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
(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?
ASKER
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.Generi c.List(Of String)' cannot be converted to 'String.'
Options.strUserCategory(3)
txt.Text = Options.strUserCategory(4)
etc.
When I try using the List class, VB tells me "Value of type 'System.Collections.Generi
Can you provide a sample of the XML (structure)?
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-
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-
Here is an example implementation that I quickly whipped up using a string array:
Produces the following output:
-saige-
<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
Produces the following output:
-saige-
SOLUTION
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
ASKER
JamesBurger, your code would work perfectly except for one thing: I'm using <System.Runtime.Serializat ion.Option alField()> 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...
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...
Did you attempt using the string array implementation I whipped up? Or does it still give you an Object reference exception?
-saige-
-saige-
Was my request (http:#answer40368065) overlooked, or is it not able to be satisfied?
ASKER
Saige, I'm using essentially the same property code as you describe:
opt.strUserCategory(1) = "Something"
I get the Object reference exception on the line: pstrUserCategory(i) = value
<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
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 likeopt.strUserCategory(1) = "Something"
I get the Object reference exception on the line: pstrUserCategory(i) = value
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.
So how can you make it work without calling the constructor, you need to change your class into a module:
Produces the following output:-saige-
My next question is are you declaring a New Options class, e.g.
Dim options as New Options
vs.
Dim options as Options
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
Produces the following output:-saige-
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:
What is the exact exception that you get? There is nothing like an ObjectReferenceException in the documentation of the Framework.
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
What is the exact exception that you get? There is nothing like an ObjectReferenceException in the documentation of the Framework.
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
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
@James - believe this is what he is referring to:
Produces the following exception:
<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
Produces the following exception:
ASKER
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):
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>
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>
My challenge at the moment is how to get from #1 to #2.
ASKER
Saige & James:
I'm definitely instantiating the class with
opt = New Options
before calling the load method of the class:
opt = Options.LoadXML(gstrOption sFolder & "\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?
I'm definitely instantiating the class with
opt = New Options
before calling the load method of the class:
opt = Options.LoadXML(gstrOption
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,
tmp = CType(myFormat.Deserialize
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?
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.
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.
ASKER
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).
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).
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
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
End If
ASKER
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)=S tring.Empt y) 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.
Sorry if I'm not communicating clearly; I need to get to bed.
ASKER CERTIFIED SOLUTION
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
SOLUTION
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
ASKER
Thanks, saige. You're right.
Thanks, all, for your help on thinking this through.
Thanks, all, for your help on thinking this through.
Not a problem. Happy coding.
-saige-
-saige-
ASKER
My comment selected as solution, because I found it independently of suggestions made; other assistance credited.
Open in new window
Or a Dictionary (if order matters):
Open in new window
In either case, once you instansiate your Options, you would reference the properties directly:
Open in new window
-saige-