Ksdmg Ksdmg - 1 month ago 11
Vb.net Question

Update XML Nodes depending on Attribute

I have the following XML Document that has been changed by a user:

<?xml version="1.0" encoding="utf-8" ?>
<Cfg xmlns="AddIn" version="161012">
<SQLConnectionString version="161012">SomeConnectionString</SQLConnectionString>
<Locale version="161012">
<Language version="161013">de-DE</Language>
<LocalSetting version="161012">en-US</LocalSetting>
</Locale>
</Cfg>


This is the initial Document:

<?xml version="1.0" encoding="utf-8" ?>
<Cfg xmlns="AddIn" version="161012">
<SQLConnectionString version="161012">SomeConnectionString</SQLConnectionString>
<Locale version="161012">
<Language version="161012">en-US</Language>
<LocalSetting version="161012">en-US</LocalSetting>
</Locale>
</Cfg>


Some user changed the Language to "de-DE". The Attribute "version" has been updated.

The user modified Document is Serialized into the following class:

<Serializable()>
Public Class Cfg

Private Shared CONFIG_LOCATION As String = GetFolderPath(SpecialFolder.ApplicationData) & "\MyProgram\"
Private Shared CONFIG_FNAME As String = "Cfg.xml"
Private Shared CONFIG_FULLPATH As String = CONFIG_LOCATION & CONFIG_FNAME
Private Shared CONFIG_ASSEMBLY_PATH As String = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) & "\cfg\"
#Region "Singleton"
Private Shared ReadOnly _instance As New System.Lazy(Of Cfg)(Function()
'Write and read
Dim _Cfg As New Cfg
If Not File.Exists(CONFIG_FULLPATH) Then
'copy xml config file
If Not Directory.Exists(CONFIG_LOCATION) Then
Directory.CreateDirectory(CONFIG_LOCATION)
End If

File.Copy(CONFIG_ASSEMBLY_PATH & CONFIG_FNAME, CONFIG_FULLPATH)
Else
'This is the point where I need to apply the updates to the xml document

End If

Dim helper = New XmlSerializerHelper(Of Cfg)()
_Cfg = helper.Read(CONFIG_FULLPATH)

Return _Cfg

End Function, System.Threading.LazyThreadSafetyMode.ExecutionAndPublication)
Public Shared ReadOnly Property Instance() As Cfg
Get
Return _instance.Value
End Get
End Property
#End Region

Private _Locale As Locale
Private _SQLConnectionString As String
Public Property Locale() As Locale
Get
Return _Locale
End Get
Set(value As Locale)
_Locale = value
End Set
End Property

Public Property SQLConnectionString As String
Get
Return _SQLConnectionString
End Get
Set(value As String)
_SQLConnectionString = value
End Set
End Property

Private Sub New()


End Sub


Public Function SaveConfigData() As Boolean
Dim helper = New XmlSerializerHelper(Of Cfg)()
Dim obj = Me
helper.Save(CONFIG_FNAME, obj)
Return True
End Function

End Class

<Serializable()>
Public Class Locale
Private _Language As String
Public Property Language As String
Get
Return _Language
End Get
Set(value As String)
_Language = value
End Set

End Property

Private _LocalSetting As String
Public Property LocalSetting As String
Get
Return _LocalSetting
End Get
Set(value As String)
_LocalSetting = value
End Set
End Property

Public Sub New()
End Sub
End Class


My Problem now is, if I update the Source XML file because the SQL Connection string has changed, I would overwrite the custom setting of the language.

Here is what I want to achieve:



New config file that has an updated ConnectionString:

<?xml version="1.0" encoding="utf-8" ?>
<Cfg xmlns="AddIn" version="161012">
<SQLConnectionString version="161013">ThisIsTheNewConnectionString</SQLConnectionString>
<Locale version="161012">
<Language version="161012">en-US</Language>
<LocalSetting version="161012">en-US</LocalSetting>
</Locale>
</Cfg>


This is how it should look like:

<?xml version="1.0" encoding="utf-8" ?>
<Cfg xmlns="AddIn" version="161012">
<SQLConnectionString version="161013">ThisIsTheNewConnectionString</SQLConnectionString>
<Locale version="161012">
<Language version="161013">de-DE</Language>
<LocalSetting version="161012">en-US</LocalSetting>
</Locale>
</Cfg>


I already tried the following:
how to Update a node in xml?
This actually worked, but i was not able to implement it into my lazy class.
This is what I also have found: How would you compare two XML Documents?
My main problem for all those solutions was, that I was not able to manage the lazy class in combination with serialization during initialization.

Answer

Because I was not able to keep the XML layout from above without help, this is what I came up with and actually it's working like a charm!

<Serializable>
<XmlRoot(ElementName:="Cfg", [Namespace]:="YourNSGoesHere")>
Public Class Cfg

    Private Shared CONFIG_LOCATION As String = GetFolderPath(SpecialFolder.ApplicationData) & "\MyProgram\"
    Private Shared CONFIG_FNAME As String = "Cfg.xml"
    Private Shared CONFIG_FULLPATH As String = CONFIG_LOCATION & CONFIG_FNAME
    Private Shared CONFIG_ASSEMBLY_PATH As String = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) & "\cfg\"
#Region "Singleton"
    Private Shared ReadOnly _instance As New System.Lazy(Of Cfg)(Function()

                                                                          Dim _Cfg As New Cfg
                                                                          Dim helper = New XmlSerializerHelper(Of Cfg)()
                                                                          Dim bDirtyFlag As Boolean
                                                                          'check if config file already exists
                                                                          If Not File.Exists(CONFIG_FULLPATH) Then                                                                              
                                                                              If Not Directory.Exists(CONFIG_LOCATION) Then
                                                                                  Directory.CreateDirectory(CONFIG_LOCATION)
                                                                              End If
                                                                              'if not, serialize standard data
                                                                              helper.Save(CONFIG_FULLPATH, _Cfg)
                                                                          Else
                                                                              'This is the point where I need to apply the updates to the xml document

                                                                          End If

                                                                          'else, read xml file
                                                                              _Cfg = helper.ReadUserConfig(CONFIG_FULLPATH)

                                                                              'example patch routine to update obsolete data in stored user files
                                                                              If _Cfg.SQLConnectionString.Version < DEF_SQLConnectionString.Version Then
                                                                                  _Cfg.SQLConnectionString = DEF_SQLConnectionString
                                                                                  bDirtyFlag = True
                                                                              End If

                                                                              If bDirtyFlag Then
                                                                                  helper.Save(CONFIG_FULLPATH, _Cfg)
                                                                              End If

                                                                          Return _Cfg

                                                                      End Function, System.Threading.LazyThreadSafetyMode.ExecutionAndPublication)
    Public Shared ReadOnly Property Instance() As Cfg
        Get
            Return _instance.Value
        End Get
    End Property
#End Region

    Private _SQLConnectionString As String
    Private _Locale As Locale
    Public Property SQLConnectionString() As PropertyModel(Of String)
        Get
            Return _SQLConnectionString
        End Get
        Set
            _SQLConnectionString = Value
        End Set
    End Property
Public Property Locale() As Locale
    Get
        Return _Locale
    End Get
    Set
        _Locale = Value
    End Set
End Property


#Region "DefaultData"
    Private Shared ReadOnly DEF_Locale = New Locale() With {
    .LocalSetting = New PropertyModel(Of String)() With {
        .Value = "en-US",
        .Version = 1476434998759
    },
    .Language = New PropertyModel(Of String)() With {
        .Value = "en-US",
        .Version = 1476434998759
    }
}
    Private Shared ReadOnly DEF_SQLConnectionString = New PropertyModel(Of String)() With {
    .Value = "SomeConnectionString",
    .Version = 1476434998791 'Timestamp of the creation. If a new value has to be applied by default, just update the timestamp
}
    Private Sub New()
        _SQLConnectionString = DEF_SQLConnectionString
        _Locale = DEF_Locale
    End Sub

#End Region


    Public Function SaveConfigData() As Boolean
        Dim helper = New XmlSerializerHelper(Of Cfg)()
        Dim obj = Me
        helper.Save(CONFIG_FNAME, obj)
        Return True
    End Function

End Class

<Serializable>
Public Class PropertyModel(Of T)
    Private _Version As Long
    Private _Value As T

    <XmlAttribute>
    Public Property Value() As T
        Get
            Return _Value
        End Get
        Set
            _Value = Value
        End Set
    End Property

    <XmlAttribute>
    Public Property Version() As Long
        Get
            Return _Version
        End Get
        Set
            _Version = Value
        End Set
    End Property
End Class

<Serializable>
Public Class Locale
    Private _Language As PropertyModel(Of String)
    Private _LocalSetting As PropertyModel(Of String)

    Public Property Language() As PropertyModel(Of String)
        Get
            Return _Language
        End Get
        Set
            _Language = Value
        End Set
    End Property

    Public Property LocalSetting() As PropertyModel(Of String)
        Get
            Return _LocalSetting
        End Get
        Set
            _LocalSetting = Value
        End Set
    End Property
End Class

'i got this code from SO but i can't remember where, credits go out to creator!
Public Class XmlSerializerHelper(Of T)
    Public _type As Type

    Public Sub New()
        _type = GetType(T)
    End Sub

    Public Sub Save(ByVal SavePath As String, obj As Object)
        Using textWriter As TextWriter = New StreamWriter(SavePath)
            Dim serializer As New XmlSerializer(_type)

            serializer.Serialize(textWriter, obj)
        End Using
    End Sub

    Public Function ReadUserConfig(ByVal LocalXMLPath As String) As T
        Dim result As T

        Using textReader As TextReader = New StreamReader(LocalXMLPath)
            Dim deserializer As New XmlSerializer(_type)

            Try
                result = DirectCast(deserializer.Deserialize(textReader), T)
            Catch ex As Exception
                MsgBox(ex.Message & Chr(13) & ex.StackTrace)
            End Try

        End Using

        Return result
    End Function
End Class

This produces the following XML output:

<?xml version="1.0" encoding="utf-8"?>
<Cfg xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="YourNSGoesHere">
  <Locale>
    <Language Value="en-US" Version="1476434998759" />
    <LocalSetting Value="en-US" Version="1476434998759" />
  </Locale>
  <SQLConnectionString Value="SomeConnectionString" Version="1476434998791" />
</Cfg>

I hope this will help someone else somewhen. I would be greatful for any additions or optimiziations.