Search code examples
.netvb.netwinformsarraylistmy.settings

ArrayList changes does not affect to My.Settings


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)

Solution

  • 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.