ArrayList changes does not affect to My.Settings

242 Views Asked by At

I'm here again trying to solve this issue with the ArrayList that I've explained in this other question... the difference is that this time I'm not searching for any alternative solution such as the usage of other external file to avoid the real problem, I would really like to fix this using My.Settings and the ArrayList, I would like to understand what is happening here about My.Settings!.

The problem is that if we set a Setting like this:

enter image description here

Then any changes performed on the setting are preserved on the next app runs, this code demostrates the problem:

Public Class Test

   Private Sub Test_Handler() Handles MyBase.Shown

       ' Create a temporal predefined ArrayList.
       Dim tmpArrayList As New ArrayList(capacity:=10I)
       With tmpArrayList
           .Add({"Item0", 0.0F})
           .Add({"Item1", 0.5F})
       End With

       ' Check the setting status.
       If My.Settings.MRU Is Nothing Then
           Debug.WriteLine("MRU setting is null.")
           Debug.WriteLine("Initializing the Setting...")
           My.Settings.MRU = New ArrayList(capacity:=10I)

       ElseIf My.Settings.MRU.Count = 0 Then
           Debug.WriteLine("MRU is not null but the ArrayList is empty.")
           Debug.WriteLine("Adding some items...")
           My.Settings.MRU = tmpArrayList.Clone

       ElseIf My.Settings.MRU.Count > 0 Then ' This part of the block  will never thrown.
           Debug.WriteLine("MRU setting is OK.")
           Debug.WriteLine("Item Count: " & CStr(My.Settings.MRU.Count))
           Threading.Thread.Sleep(Integer.MaxValue)

       End If

       Debug.WriteLine("Saving any changes")
       My.Settings.Save()

       Debug.WriteLine("Updating any changes")
       My.Settings.Reload()

       Debug.WriteLine(String.Empty)
       Debug.WriteLine("****************************************")
       Debug.WriteLine("Checking again the MRU setting status in...")
       For Count As Integer = 1 To 3
           Debug.WriteLine(CStr(Count) & New String("."c, Count))
           Threading.Thread.Sleep(TimeSpan.FromSeconds(1))
       Next
       Debug.WriteLine("****************************************")
       Debug.WriteLine(String.Empty)

       Me.Test_Handler()

   End Sub

End Class

Someone could teach me to understand why the ArrayList is not really saved, or show me how to solve this issue?.

UPDATE

Ok I've write this serializable class trying to solve this issue:

''' <summary>
''' A Class intended to use it as an Item for a MRU item collection that stores the item filepath, with additional info.
''' </summary>
<Serializable()>
Public Class MostRecentUsedItem

    ''' <summary>
    ''' Gets or sets the item filepath.
    ''' </summary>
    ''' <value>The file path.</value>
    Public Property FilePath As String

    ''' <summary>
    ''' (Optionally) Gets or sets the item index.
    ''' </summary>
    ''' <value>The index.</value>
    Public Property Index As Integer

    ''' <summary>
    ''' (Optionally) Gets or sets the item image.
    ''' </summary>
    ''' <value>The image.</value>
    Public Property Img As Bitmap

    ''' <summary>
    ''' (Optionally) Gets or sets the item last-time open date.
    ''' </summary>
    ''' <value>The index.</value>
    Public Property [Date] As Date

    ''' <summary>
    ''' (Optionally) Gets or sets the item tag.
    ''' </summary>
    ''' <value>The tag object.</value>
    Public Property Tag As Object

End Class

Also I've write this helper function to help me in this issue:

''' <summary>
''' Determines whether an object can be XML serialized.
''' </summary>
''' <param name="Object">The object.</param>
''' <returns><c>true</c> if object is XML serializable; otherwise, <c>false</c>.</returns>
Private Function IsObjectSerializable(ByVal [Object] As Object) As Boolean

    Using fs As New IO.FileStream(IO.Path.GetTempFileName, IO.FileMode.Create)

        Dim Serializer As New Xml.Serialization.XmlSerializer([Object].GetType)

        Try
            Serializer.Serialize(fs, [Object])
            Return True

        Catch ex As InvalidOperationException
            Return False

        End Try

    End Using

End Function

At the moment that I initialize the setting like this, it is serializable:

My.Settings.MRU = New ArrayList

At the moment that I add just a string, it still be serializable:

My.Settings.MRU.Add("test string")

But at the moment that I try to add my serializable class, or any other kind of datatype like a String(), the ArrayList begins unserializable, Like this:

My.Settings.MRU.Add({"Collection", "Of", "Strings"})

Or like this else:

Dim MRUItem As New MostRecentUsedItem
MRUItem.FilePath = "C:\Test.ext"
My.Settings.MRU.Add(MRUItem)

...So the ArrayList contents are not preserved on the next run, can't be serialized.

I also tried to change the setting type from System.Collections.ArrayList to System.Object (desperately) so now I can do this, but the problem persist, I mean the collection is not saved on the next app run:

My.Settings.MRU = New List(Of MostRecentUsedItem)
Dim MRUItem As New MostRecentUsedItem
MRUItem.FilePath = "C:\Test.ext"
My.Settings.MRU.Add(MRUItem)
3

There are 3 best solutions below

3
On BEST ANSWER

Since Application Settings serializes complex types as XML, you must make sure that the specific type can be serialized as XML before saving it.

You can test your data type with the following method:

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    Dim tmpArrayList As New ArrayList()
    With tmpArrayList
        .Add({"Item0", 0.0F})
        .Add({"Item1", 0.5F})
    End With

    Dim XmlSerializer1 As New XmlSerializer(tmpArrayList.GetType)
    XmlSerializer1.Serialize(Console.Out, tmpArrayList)
End Sub

This code fails to Serialize the ArrayList and returns the following message:

An unhandled exception of type 'System.InvalidOperationException' occurred in System.Xml.dll Additional information: There was an error generating the XML document.

But if you try to store simple data type in the ArrayList, the Serialization will succeed

Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
    Dim tmpArrayList As New ArrayList()
    With tmpArrayList
        .Add("Item0")
        .Add("Item1")
    End With

    Dim XmlSerializer1 As New XmlSerializer(tmpArrayList.GetType)
    XmlSerializer1.Serialize(Console.Out, tmpArrayList)
End Sub

The same happens when you store data in the Application Settings, but the difference is that it doesn't return errors.

Useful links:

EDIT

Implementation using DataTable

Create a new Windows Forms Project, add a new setting called NewMRU in the Application Settings with a data type of System.Data.DataTable and try the following code.

Imports System.IO
Imports System.Xml.Serialization
Imports System.Drawing.Imaging

Public Class Form1

    Dim DataTable1 As New DataTable("MySettingsDataTable")

    Private Sub Form1_Shown(sender As Object, e As EventArgs) Handles Me.Shown
        DataTable1.Columns.Add("FilePath", GetType(String))
        DataTable1.Columns.Add("Index", GetType(Integer))
        DataTable1.Columns.Add("Img", GetType(Byte()))
        DataTable1.Columns.Add("Date", GetType(DateTime))
    End Sub

    Private Sub button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        AddPropertyRow("C:\Temp", 1, GetBytesFromBitmap(Me.Icon.ToBitmap), Now)
        AddPropertyRow("C:\Windows", 2, GetBytesFromBitmap(Me.Icon.ToBitmap), Now)

        My.Settings.NewMRU = DataTable1
        My.Settings.Save()

        'MsgBox(IsObjectSerializable(DataTable1))
    End Sub

    Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
        If Not My.Settings.NewMRU Is Nothing Then
            Dim i As Integer

            For i = 0 To My.Settings.NewMRU.Rows.Count - 1
                Debug.WriteLine("Row - " & i + 1)
                Debug.WriteLine("FilePath = " & My.Settings.NewMRU.Rows(i).Item("FilePath"))
                Debug.WriteLine("Index = " & My.Settings.NewMRU.Rows(i).Item("Index"))
                Debug.WriteLine("Img bytes count = " & TryCast(My.Settings.NewMRU.Rows(i).Item("Img"), Byte()).Length)
                Debug.WriteLine("Date = " & My.Settings.NewMRU.Rows(i).Item("Date"))
                Debug.WriteLine("")
            Next
        End If

        'PictureBox1.Image = GetBitmapFromBytes(TryCast(My.Settings.NewMRU.Rows(0).Item("Img"), Byte()))
    End Sub

    Private Sub AddPropertyRow(ByVal FilePath As String, ByVal Index As Integer,
                               ByVal Img() As Byte, ByVal [Date] As Date)
        DataTable1.Rows.Add(DataTable1.NewRow)
        Dim RowIndex As Integer = DataTable1.Rows.Count - 1
        DataTable1.Rows(RowIndex).Item("FilePath") = FilePath
        DataTable1.Rows(RowIndex).Item("Index") = Index
        DataTable1.Rows(RowIndex).Item("Img") = Img
        DataTable1.Rows(RowIndex).Item("Date") = [Date]
    End Sub

    Private Function IsObjectSerializable(ByVal [Object] As Object) As Boolean
        Using fs As New IO.FileStream(IO.Path.GetTempFileName, IO.FileMode.Create)
            Dim Serializer As New Xml.Serialization.XmlSerializer([Object].GetType)

            Try
                Serializer.Serialize(fs, [Object])
                Return True
            Catch ex As InvalidOperationException
                Return False
            End Try
        End Using
    End Function

    Private Function GetBytesFromBitmap(ByVal Bitmap1 As Bitmap) As Byte()
        Dim BitmapBytes() As Byte

        Using MemoryStream1 As New MemoryStream()
            Bitmap1.Save(MemoryStream1, ImageFormat.Bmp)
            BitmapBytes = MemoryStream1.GetBuffer()
        End Using

        Return BitmapBytes
    End Function

    Private Function GetBitmapFromBytes(ByVal Bytes As Byte()) As Bitmap
        Dim Bitmap1 As Bitmap = Nothing

        Using MemoryStream1 As New MemoryStream(Bytes, 0, Bytes.Length)
            Bitmap1 = Image.FromStream(MemoryStream1)
        End Using

        Return Bitmap1
    End Function

End Class

If you want to use the Tag property (which I have omitted from the code since it's not serializable), you should split its values (Item, Value) as columns in the DataTable.

2
On

Why you say? Because you treat an Integer like it's a Boolean.

The following line will always be true (the condition are met) because the Count property will never return -1.

ElseIf Not My.Settings.MRU.Count Then

That's why this line is never reached.

ElseIf My.Settings.MRU.Count Then 

What you should do is replace your code with this:

ElseIf My.Settings.MRU.Count = 0 Then
Else

And, as always, set Option Strict to On.


Simple test

For i As Integer = -2 To 2
    Debug.Write(i.ToString())
    If (Not i) Then
        Debug.Write(", Not i")
    ElseIf (i) Then
        Debug.Write(", i")
    End If
    Debug.Write(Environment.NewLine)
Next

Result:

-2, Not i
-1, i
 0, Not i
 1, Not i
 2, Not i
0
On

This wont work with an ArrayList in My.Settings:

.Add({"Item0", 0.0F})
My.Settings.MRU.Add({"Collection", "Of", "Strings"})

For whatever reason, My.Settings cant/wont serialize when each element is an array of data. In the first, the types are mixed which might contribute. Using simple data and AddRange it will:

.AddRange("Foo1", "Bar2", "FooBar")

It also wont serialize an ArrayList of objects, ie MostReccentItem. I am not sure why, but in both cases it seems the graph is too complex for what My.Settings it is expecting or how it is internally doing it.

I think you should accept that either what you are trying to do, or perhaps how you are trying to do it, is too complex for My.Settings. If you are tinkering with serializers, you are one small step from simply doing it yourself:

You already have this:

<Serializable()>
Public Class MostRecentUsedItem

    Public Property FilePath As String

    Public Property Index As Integer

    Public Property Img As Bitmap

    Public Property [Date] As Date

    Public Property Tag As Object

End Class

Add a List to replace the My.Settings container, and add a filename var:

Private SettingsFile As String = "C:\Temp\MRUSetTest.bin"
Private MRUList As New List(Of MostRecentUsedItem)

Change the serialization code to this for saving (I'm using BF):

Dim bf As New BinaryFormatter
Using fs As New FileStream(SettingsFile , FileMode.OpenOrCreate)
    bf.Serialize(fs, MRUList)
End Using

Another small block to load the data:

' ToDo: add am If Exists line for the very first time the app runs.

Dim bf As New BinaryFormatter
Using fs As New FileStream(SettingsFile , FileMode.Open)
    MRUList = CType(bf.Deserialize(fs), List(Of MostRecentUsedItem))
End Using

There is simply nothing Magical about My.Settings that it is worth all the effort you are exerting to try and get it to work. The above will save/load your List, which should be the important point.

In more complex apps it is very simple to make MRUList a member of another class which holds other Settings such as user options, and simply serialize that larger class.

You can use the XMLSerializer instead, though that adds some limitations. I rather like that using the BinaryFormatter, the user cannot find the settings file and tinker with it as easily.