Johan Stenberg's blog

  • My setting values for collection types are lost when I save my settings!?

    The client configuration runtime will use (in order) the following methods to serialize/deserialize setting values

    1) The sheme indicated by the SettingsSerializeAs attribute

    2) TypeConverter.To/FromString() for the type converter associated with the type of the setting if it exists and supports converting to/from strings

    3) XML Serialization

    If you want to use a type that doesn't have the appropriate type converter and doesn't support XML serialization well (which happens to be true for quite a few of the collection types) you can still take advantage of client config by providing your own type converter. Below is a sample that (hopefully) will show what can be done. The sample uses the base64 encoded representation of the binary serialized values as the string representation for the value - this is not exactly easily read by humans, so depending on your specific requirements, you may want to use a different string representation of your data...

    Option Strict On

    Module Module1

        Public Sub Main()
            ' Associate a type descriptor with the setting type. This could also be done by putting a TypeDescriptionProvider attribute
            ' on the type declaration itself.
            System.ComponentModel.TypeDescriptor.AddProvider( _
                New SerializableTypeDescriptionProvider(Of System.Collections.Generic.Dictionary(Of String, String)), _
                GetType(System.Collections.Generic.Dictionary(Of String, String)))

            ' Create an instance of my test class
            Dim s As New MyTestSettingsClass
            s.Foo = New System.Collections.Generic.Dictionary(Of String, String)
            s.Foo.Add("Bar", "Zoo")
            s.Foo.Add("Doo", "Goo")
            s.Save()

            ' Load the same settings again...
            Dim s2 As New MyTestSettingsClass
            Debug.WriteLine(s2.Foo.Count)
        End Sub

        ''' <summary>
        '''
        ''' </summary>
        ''' <typeparam name="T"></typeparam>
        ''' <remarks></remarks>
        Public Class SerializableTypeDescriptionProvider(Of T)
            Inherits System.ComponentModel.TypeDescriptionProvider

            Public Overrides Function GetTypeDescriptor(ByVal objectType As System.Type, ByVal instance As Object) As System.ComponentModel.ICustomTypeDescriptor
                If Not objectType Is GetType(T) Then
                    Return MyBase.GetTypeDescriptor(objectType, instance)
                End If

                Dim parentTypeDescriptor As System.ComponentModel.ICustomTypeDescriptor = MyBase.GetTypeDescriptor(objectType, instance)
                If parentTypeDescriptor IsNot Nothing Then
                    Dim existingTypeConverter As System.ComponentModel.TypeConverter = _
                        parentTypeDescriptor.GetConverter()
                    If existingTypeConverter IsNot Nothing _
                        AndAlso existingTypeConverter.CanConvertFrom(GetType(String)) _
                        AndAlso existingTypeConverter.CanConvertTo(GetType(String)) _
                    Then
                        ' If the parentdescriptor already supported to/from string, we might as well
                        ' use that... We don't really *have* to check this, but...
                        Return MyBase.GetTypeDescriptor(objectType, instance)
                    End If
                End If

                Return New SerializableConverterProvider(Of T)(parentTypeDescriptor)
            End Function
        End Class

        ' Simple generic implementation that will provide a type converter
        ' that supports to/from string using base64 encoded string based on
        ' binary serialization
        Public Class SerializableConverterProvider(Of T)
            Inherits System.ComponentModel.CustomTypeDescriptor

            Sub New(ByVal parentTypeDescriptor As System.ComponentModel.ICustomTypeDescriptor)
                MyBase.New(parentTypeDescriptor)
            End Sub

            Public Overrides Function GetConverter() As System.ComponentModel.TypeConverter
                Return New SerializeAsStringTypeConverter()
            End Function

            Private Class SerializeAsStringTypeConverter
                Inherits System.ComponentModel.TypeConverter

                Public Overrides Function CanConvertFrom(ByVal context As System.ComponentModel.ITypeDescriptorContext, ByVal sourceType As System.Type) As Boolean
                    If sourceType Is GetType(String) Then
                        Return True
                    Else
                        Return MyBase.CanConvertFrom(context, sourceType)
                    End If
                End Function

                Public Overrides Function CanConvertTo(ByVal context As System.ComponentModel.ITypeDescriptorContext, ByVal destinationType As System.Type) As Boolean
                    If destinationType Is GetType(String) Then
                        Return True
                    Else
                        Return MyBase.CanConvertTo(context, destinationType)
                    End If
                End Function

                Public Overrides Function ConvertTo(ByVal context As System.ComponentModel.ITypeDescriptorContext, ByVal culture As System.Globalization.CultureInfo, ByVal value As Object, ByVal destinationType As System.Type) As Object
                    If destinationType Is GetType(String) Then
                        Dim serializer As New System.Runtime.Serialization.Formatters.Binary.BinaryFormatter()
                        Dim stream As New System.IO.MemoryStream
                        serializer.Serialize(stream, value)
                        Return System.Convert.ToBase64String(stream.ToArray())
                    Else
                        Return MyBase.ConvertTo(context, culture, value, destinationType)
                    End If
                End Function

                Public Overrides Function ConvertFrom(ByVal context As System.ComponentModel.ITypeDescriptorContext, ByVal culture As System.Globalization.CultureInfo, ByVal value As Object) As Object
                    If TypeOf value Is String Then
                        Dim serializer As New System.Runtime.Serialization.Formatters.Binary.BinaryFormatter()
                        Dim bytes() As Byte = System.Convert.FromBase64String(DirectCast(value, String))
                        Dim stream As New System.IO.MemoryStream(bytes)
                        Return serializer.Deserialize(stream)
                    Else
                        Return MyBase.ConvertFrom(context, culture, value)
                    End If
                End Function

            End Class
        End Class

        ' Since the settings designer isn't very good at handling generic types,
        ' I have created a (very) simplistic class that expose a setting of a type
        ' that ordinarily would have been problematic to persist...
        Public Class MyTestSettingsClass
            Inherits System.Configuration.ApplicationSettingsBase

            <System.Configuration.UserScopedSetting()> _
            Public Property Foo() As System.Collections.Generic.Dictionary(Of String, String)
                Get
                    Return CType(Me("Foo"), System.Collections.Generic.Dictionary(Of String, String))
                End Get
                Set(ByVal value As System.Collections.Generic.Dictionary(Of String, String))
                    Me("Foo") = value
                End Set
            End Property

        End Class
    End Module

    This posting is provided "AS IS" with no warranties, and confers no rights.

     

     

  • Supporting multiple set of values for your settings class

    A common scenario is trying to maintain different values for settings when deploying for test vs the "real" release of your project. In beta1 of Visual Studio 2005, the settings designer had the concept of a "Settings profile" that was intended to at least partially solve this issue. You picked the currently active profile in the setitngs designer, and it would generate the appropriate values for the DefaultSettingValueAttribute as well as in app.config. Unfortunately, it's shortcomings were so severe that we decided to cut this functionality in the released version of VS2005 and concentrate on getting it right for the next version of Visual Studio.

    So, where does this leave you? Is there any way that I can have multiple set of values for a given settings class? As it turns out, you can simulate this behavior, but it requires some manual work, and it also uses a property of the ApplicationSettingsBase class in ways that it wasn't nescessarily intended to be used in.

    The ApplicationSettingsBase class has a property called SettingsKey that can be used to support having multiple instances of the same settings class, all having it's own set of values. When would you ever want to do something like that? Well, consider writing UserControl that wants to persist some of its properties. Simple, right? You create a settings class using the settings designer and simply save the settings when your user control gets Disposed. But wait a minute - what happens if the user that wants to have multiple instances of your user control on the same form? It is not very likely that they all want to share the same settings... That's where the SettingsKey property comes in. By settings the SettingsKey, you can have named instances of your settings class, and they would all get their own area of the user.config file to scribble their values in. For a settings class associated with a user control, I would probably use the control's Name as the settings key.

    So, how can I (ab)use the SettingsKey property to accomplish what I want? Well, I could make sure that I use different setting key values depending on which set of values that I wanted to be used. If I wanted a specific set of values to be used in my test environment, I could make sure that I set the SettingsKey to "Test". But how would I persist the SettingsKey? Well, since we are discussing settings here, let's use a setting to store the SettingsKey that we want to use [:D]

    I start by adding a new settings file and call it SettingsProfile.Settings. I add a single application scoped string value called "CurrentProfile" and set it's default value to an empty string. This is (unsurprisingly enough) where we will store the name of the current profile we want to use...

    Next, I open the settings designer for the class that I want to have different values for depending on the configuration and click on the View Code button on the designer's toolbar. Unless you are using J# (which doesn't support partial classes), this will open up the part of the generated settings class where the user can freely edit stuff. We add a parameterless constructor which only needs to do one thing; assign the value SettingsKey property:

    Public Class MySettings
       Public Sub New() 
          Me.SettingsKey = SettingsProfile.Default.CurrentProfile
       End Sub
    End Class

    Now, if we want to change the value of a setting in the "Test" profile, we can go into the app.config file, I can add a new sectionGroup declaration in my app.config file:

    <configSections>
       <sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
          <section name="ConsoleApplication116.SettingsProfile" type="System.Configuration.ClientSettingsSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
          <section name="ConsoleApplication116.My.MySettings" type="System.Configuration.ClientSettingsSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
          <section name="ConsoleApplication116.My.MySettings.Test" type="System.Configuration.ClientSettingsSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
       </sectionGroup>
    </configSections>

    And add the corresponding value in the the applicationSettings section of the app.config file - in the snippet below, I changed the value of the setting called "Onk", and changed the current "profile" to Test

    <applicationSettings>
       <ConsoleApplication116.SettingsProfile>
          <setting name="Profile" serializeAs="String">
             <value>Test</value>
          </setting>
       </ConsoleApplication116.SettingsProfile>
       
    <ConsoleApplication116.My.MySettings>
          <setting name="Onk" serializeAs="String">
             <value>Release value</value>
          </setting>
       </ConsoleApplication116.My.MySettings>
       <ConsoleApplication116.My.MySettings.Test>
          <setting name="Onk" serializeAs="String">
             <value>Test value</value>
          </setting>
       </ConsoleApplication116.My.MySettings.Test>
    </applicationSettings>

    So, what is the downside of doing this? Well, first of all, it assumes that you weren't already using multiple instances of the settings class with different setting keys. Second, if you have any sensitive data that you don't want to distribute once the application is ready to be released, you have to make sure that you clean up your app.config file.


© 2009 Microsoft Corporation. All rights reserved. Terms of Use  |  Trademarks  |  Privacy Statement
Microsoft
Page view tracker