다음을 통해 공유


VB.NET: My.Settings alternate for storing app settings

Introduction

Microsoft provides My.Settings Object which provides access to application settings and allows a developer to dynamically store and retrieve property settings and other information for your application. To create settings at design time is done using an interface under project settings as described in the following documentation

There are fallacies when dealing with complex read and retrieval of setting like storing list or user defined data like storing information stored in a class instance. Several concepts are presented here which moves away from My.Settings using a section known as appSettings and sectionGroup which provide reading and retrieval of simple and complex data stored in an application’s configuration file.

Requires

A reference to System.Configuration for working with settings in an application configuration files.

Throughout the article this app.config file is used.

  • Lines 4 to 7 point to lines 25 to 47 for storing multiple email addresses.
  • Lines 13 to 23 store information to be used in the application. All values are stored as string while code presented here transforms strings to proper types

Sample configuration file

01.<?xml version="1.0" encoding="utf-8" ?>
02.<configuration>
03. <configSections>
04. <sectionGroup name="mailSettings">
05.  <section name="smtp_Home" type="System.Net.Configuration.SmtpSection"/>
06.  <section name="smtp_Work" type="System.Net.Configuration.SmtpSection"/>
07. </sectionGroup>
08. </configSections>
09. 
10. <startup>
11. <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
12. </startup>
13. <appSettings>
14. <add key="MainWindowTitle" value = "Code sample for reading/setting app.config" />
15. <add key="IncomingFolder" value = "D:\UISides\oed_incoming" />
16. <add key="TestMode" value = "false" />
17. <add key="importMinutesToPause" value="2" />
18. <add key="LastCategoryIdentifier" value="-1" />
19. <add key="LastRan" value="10/31/2020 3:12:50 AM" />
20. <add key="DatabaseServer" value=".\SQLEXPRESS" />
21. <add key="Catalog" value="NorthWind2020" />
22. <add key="UserDetails" value="" />
23. </appSettings>
24. 
25. <mailSettings>
26. <smtp_Home from="someone@gmail.com">
27.  <network
28.  host="smtp.gmail.com"
29.  port="587"
30.  enableSsl="true"
31.  userName="MssGMail"
32.  password=""
33.  defaultCredentials="false" />
34.  <specifiedPickupDirectory pickupDirectoryLocation="MailDrop"/>
35. </smtp_Home>
36. 
37. <smtp_Work from="karenpayneoregon@gmail.com">
38.  <network
39.  host="smtp.gmail.com"
40.  port="587"
41.  enableSsl="true"
42.  userName="oregon@gmail.com"
43.  password=""
44.  defaultCredentials="false" />
45.  <specifiedPickupDirectory pickupDirectoryLocation="MailDrop"/>
46. </smtp_Work>
47. </mailSettings>
48.</configuration>

Application setting class

To properly interact with settings in a application file the following class provides read and write operations. 

There are delegates/events which allow a developer to monitor changes along with runtime exceptions which a class or form may subscribe to as callbacks shown below. The reason for events is so developers need not wrap code which interacts with a configuration file need not be wrapped in a try/catch statement in calling code.

Namespace Classes
 Public Class  ApplicationSettings
 
  Public Delegate  Sub OnErrorSettingDelegate(args As ApplicationSettingError)
  Public Delegate  Sub OnErrorGetDelegate(key As String, ex As  Exception)
  Public Delegate  Sub OnSettingChangedDelegate(key As String, value As  String)
 
  ''' <summary>
  ''' Provides access to when an exception is thrown when a value can not be set
  ''' </summary>
  Public Shared  Event OnSettingsErrorEvent As OnErrorSettingDelegate
  ''' <summary>
  ''' provides access when a key in the configuration file is not found in the configuration file
  ''' </summary>
  Public Shared  Event OnGetKeyErrorEvent As OnErrorGetDelegate
  ''' <summary>
  ''' Provides access to a setting changed
  ''' </summary>
  Public Shared  Event OnSettingChangedEvent As OnSettingChangedDelegate

Basic read operation is done with the method below which resides in ApplicationSettings class. Note in the catch, Expections.wite(e), this is a method found here to write to a text log file.

''' <summary>
''' Get app setting as string from application file. If configKey is not located an exception is thrown without
''' anything to indicate this but will throw a runtime exception from the calling method.
''' </summary>
''' <param name="configKey">Key in app.config</param>
''' <returns>Key value or an empty string</returns>
Public Shared  Function GetSettingAsString(configKey As String) As  String
 
 Dim value As String  = Nothing
 
 Try
 
  value = ConfigurationManager.AppSettings(configKey)
 
  If value Is Nothing  Then
   Throw New  Exception($"Setting {configKey} not found")
  End If
 
 Catch e As Exception
  RaiseEvent OnGetKeyErrorEvent(configKey, e)
  Exceptions.Write(e)
 End Try
 
 Return value
 
End Function

To get the main window title on line 14 of app.config which is generic, pass in a property found under appSettings and a value is returned.

GetSettingAsString("IncomingFolder")

This works fine when there are only a few settings while like the configuration file above there are many settings. In these cases a wrapper method allows a developer to work with settings easily. Below is a wrapper for obtaining IncomingFolder value.

''' <summary>
''' Get incoming folder
''' </summary>
''' <returns></returns>
Public Shared  Function GetIncomingFolder() As String
 Return GetSettingAsString("IncomingFolder")
End Function

Usage

Me.Text = ApplicationSettings.GetIncomingFolder()

For long time VB.NET developers use to My.Settings a wrapper can be created under My.Settings as presented in the following class. Now the following is possible.

Me.Text = My.Settings.MainWindowTitle

Reading types other than strings

The following methods provide type conversion from string to specific types.

There are many more types to convert from string to a specific type, by following one of the above methods other methods may be added e.g. GetSettingAsBoolean or perhaps a class instance or a setting with multiple values.

Suppose rather than reading one setting at a time a class is created to read all settings at once as shown below.

Namespace Classes
 Public Class  MyApplication
  Public Property  MainWindowTitle() As  String
  Public Property  IncomingFolder() As  String
  Public Property  ImportMinutesToPause() As Integer
  Public Property  TestMode() As  Boolean
  Public Property  LastRan() As  DateTime
  Public Property  DatabaseServer() As  String
  Public Property  Catalog() As  String
  Public ReadOnly  Property ConnectionString() As String
   Get
    Return $"Data Source= {DatabaseServer};Initial Catalog={Catalog};Integrated Security=True"
   End Get
  End Property
 End Class
End Namespace

Back in ApplicationSettings class the following method reads property values to the proper types,

Public Shared  Function Application() As MyApplication
 
 Return New  MyApplication() With  {
  .IncomingFolder = GetIncomingFolder(),
  .MainWindowTitle = MainWindowTitle(),
  .DatabaseServer = GetSettingAsString("DatabaseServer"),
  .Catalog = GetSettingAsString("Catalog"),
  .LastRan = GetSettingAsDateTime("LastRan")
 }
 
End Function

This can be done dynamically also as per the following method. This method need not know property names.

''' <summary>
''' Populate <see cref="MyApplication"/> instance dynamically
''' </summary>
''' <returns></returns>
Public Shared  Function CreateMyApplicationDynamically()  As  MyApplication
 
 Dim propertyInfo = GetType(MyApplication).
   GetProperties().
   Where(Function(p) p.CanWrite).
   Select(Function(p) New  With {
      .PropertyName = p.Name,
      .Value = ConfigurationManager.AppSettings(p.Name)
      }).
   ToList()
 
 Dim myApplication As New  MyApplication()
 
 For Each  anonymous In  propertyInfo
  Dim propertyValue As Object  = anonymous.Value
  myApplication.SetPropertyValue(anonymous.PropertyName, propertyValue)
 Next
 
 
 Return myApplication
 
End Function

Mail section

In the application file shown above there is a mailSettings section which allows defining values for sending SMTP email messages. Note that passwords are plain text which is unwise, to handle this simply use a encrypt/decrypt methods so peeking eye's can not see passwords which would be done in the ApplicationSettings class with methods for encrypt and decryption in a helper class or code module.

For reading SMTP mail setting see MailConfiguration class into an instance the MailItem class. Now the following method reads in the (in this case) two settings for mail configurations.

Public Shared  Function MailAddresses() As List(Of MailItem)
 Dim emailList As New  List(Of MailItem)
 Dim config As Configuration = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None)
 Dim myCollect As ConfigurationSectionGroupCollection = config.SectionGroups
 
 For Each  configurationSectionGroup As ConfigurationSectionGroup In myCollect
 
  For Each  configurationSection As ConfigurationSection In configurationSectionGroup.Sections
   Dim sectionName As String  = configurationSection.SectionInformation.Name.ToString()
 
 
   If sectionName.StartsWith("smtp") Then
    Dim mc As MailConfiguration = New MailConfiguration($"mailSettings/{sectionName}")
    Dim mailItem As New  MailItem With  {
      .DisplayName = sectionName.Replace("smtp_", ""),
      .ConfigurationName = sectionName, .MailConfiguration = mc
      }
    emailList.Add(mailItem)
   End If
  Next
 Next
 
 Return emailList
 
End Function

A property is used in the custom My.Settings class to return a List(Of MailItem).

''' <summary>
''' Get mail addresses for app.config
''' </summary>
''' <returns></returns>
Public ReadOnly  Property MailAddresses() As List(Of MailItem)
 Get
  Return ApplicationSettings.MailAddresses()
 End Get
End Property

Which can be used in a ComboBox or other control to display and allow a user to select for sending email messages.

MailItemsComboBox.DataSource = My.Settings.MailAddresses

Get a MailItem from the ComboBox.

''' <summary>
''' Get current selected email details from MailItemsComboBox
''' </summary>
''' <param name="sender"></param>
''' <param name="e"></param>
Private Sub  CurrentMailItemButton_Click(sender As Object, e As  EventArgs) Handles  CurrentMailItemButton.Click
 Dim mailItem = CType(MailItemsComboBox.SelectedItem, MailItem)
 MessageBox.Show($"From: [{mailItem.From}]{Environment.NewLine}User name: [{mailItem.UserName}]")
End Sub

Store/read a class instance

In the above examples all properties are stored in individual properties in the configuration file, an easy method to store a class inside of the application file is with JSON and Newtonsoft.Json NuGet package.

Example, store a person's first, last name and email address.

Namespace Classes
 Public Class  UserDetails
  Public Property  FirstName() As  String
  Public Property  LastName() As  String
  Public Property  EmailAddress() As  String
 End Class
 
 Public Class  Mocking
  Public Shared  Function CreateUser() As UserDetails
   Return New  UserDetails() With  {.FirstName = "", .LastName = "", .EmailAddress = ""}
  End Function
 End Class
End Namespace

In ApplicationSettings class, read write opertions.

''' <summary>
''' Write UserDetails key to app.config
''' </summary>
''' <param name="userDetails"></param>
Public Shared  Sub SerializeUserDetails(userDetails As UserDetails)
 Dim details = JsonConvert.SerializeObject(userDetails)
 SetValue("UserDetails", details)
End Sub
''' <summary>
''' Read UserDetails key from app.config
''' </summary>
''' <returns></returns>
Public Shared  Function DeserializeUserDetails() As UserDetails
 Dim json = GetSettingAsString("UserDetails")
 Return JsonConvert.DeserializeObject(Of UserDetails)(json)
End Function

Assertion

To prevent runtime exceptions if  key does not exists the following method accepts a key to see if the key exists. Use this method when for development and/or when a key may not exists e.g. a user edited a key name and renamed the key.

''' <summary>
''' Determine if a key exists
''' </summary>
Public Shared  Function KeyExists(key As String) As  Boolean
 Dim result = ConfigurationManager.AppSettings.AllKeys.FirstOrDefault(Function(keyName) keyName = key)
 Return result IsNot Nothing
End Function

Writing back to configuration file

To set a key value is a three step process, first set a key value, save the changes followed by refreshing the configuration file which is done in SetValue method. In the event of a runtime exception in the catch statement an event is raised to notifiy subscribers something went wrong and writes the exception to the application error log.

JSON configuration

Another option is to forego using app.config, instead use a json file for application settings. The following class represents the same properties mentioned above except for mail configuration items to keep things simple.

Imports System.ComponentModel
Imports System.Runtime.CompilerServices
 
Namespace Classes.Json
 <Serializable()>
 Public Class  MyApplicationJson
  Implements INotifyPropertyChanged
 
  Private _mainWindowTitle As String
  Private _incomingFolder As String
  Private _importMinutesToPause As Integer
  Private _testMode As Boolean
  Private _lastRan As Date
  Private _databaseServer As String
  Private _catalog As String
  Private _lastCategoryIdentifier As Integer
 
 
  Public Property  MainWindowTitle() As  String
   Get
    Return _mainWindowTitle
   End Get
   Set
    _mainWindowTitle = Value
    OnPropertyChanged()
   End Set
  End Property
 
  Public Property  IncomingFolder() As  String
   Get
    Return _incomingFolder
   End Get
   Set
    _incomingFolder = Value
    OnPropertyChanged()
   End Set
  End Property
 
  Public Property  ImportMinutesToPause() As Integer
   Get
    Return _importMinutesToPause
   End Get
   Set
    _importMinutesToPause = Value
    OnPropertyChanged()
   End Set
  End Property
 
  Public Property  TestMode() As  Boolean
   Get
    Return _testMode
   End Get
   Set
    _testMode = Value
    OnPropertyChanged()
   End Set
  End Property
 
  Public Property  LastRan() As  DateTime
   Get
    Return _lastRan
   End Get
   Set
    _lastRan = Value
    OnPropertyChanged()
   End Set
  End Property
 
  Public Property  LastCategoryIdentifier() As Integer
   Get
    Return _lastCategoryIdentifier
   End Get
   Set
    _lastCategoryIdentifier = Value
    OnPropertyChanged()
   End Set
  End Property
 
  Public Property  DatabaseServer() As  String
   Get
    Return _databaseServer
   End Get
   Set
    _databaseServer = Value
    OnPropertyChanged()
   End Set
  End Property
 
  Public Property  Catalog() As  String
   Get
    Return _catalog
   End Get
   Set
    _catalog = Value
    OnPropertyChanged()
   End Set
  End Property
 
  Public ReadOnly  Property ConnectionString() As String
   Get
    Return $"Data Source= {DatabaseServer};Initial Catalog={Catalog};Integrated Security=True"
   End Get
  End Property
  Public Overrides  Function ToString() As String
   Return $"Last ran {LastRan}"
  End Function
 
  Public Event  PropertyChanged As  PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
  Protected Overridable  Sub OnPropertyChanged(<CallerMemberName>  Optional  memberName As  String = Nothing)
   RaiseEvent PropertyChanged(Me, New  PropertyChangedEventArgs(memberName))
  End Sub
  ''' <summary>
  ''' Prevent serializing ConnectionString property
  ''' </summary>
  ''' <returns></returns>
  Public Function  ShouldSerializeConnectionString() As Boolean
   Return False
  End Function
 End Class
 
End Namespace

File operations are performed in the following class. Note MockedUp method used for this article to show how to initialize with values rather then null values.

Imports System.IO
Imports Newtonsoft.Json
 
Namespace Classes.Json
 
 Public Class  JsonFileOperations
  Private Property  mFileName() As  String
  Public Sub  New()
 
  End Sub
 
  Public Sub  New(fileName As String)
   mFileName = fileName
  End Sub
  Public Function  LoadApplicationData(fileName As String) As  MyApplicationJson
 
   Using streamReader = New  StreamReader(fileName)
    Dim json = streamReader.ReadToEnd()
    Return JsonConvert.DeserializeObject(Of MyApplicationJson)(json)
   End Using
 
  End Function
  Public Sub  SaveApplicationData(searchItems As MyApplicationJson, fileName As String)
 
   Using streamWriter = File.CreateText(fileName)
 
    Dim serializer = New JsonSerializer With {.Formatting = Formatting.Indented}
    serializer.Serialize(streamWriter, searchItems)
 
   End Using
 
  End Sub
 
  Public Shared  Sub MockUp()
   If Not  File.Exists(My.Settings.JsonFileName) Then
    Dim appSetting = New MyApplicationJson With {
      .MainWindowTitle = "Code sample for reading/setting app.config",
      .IncomingFolder = "D:\UISides\oed_incoming",
      .TestMode = False,
      .ImportMinutesToPause = 2,
      .LastCategoryIdentifier = 3,
      .LastRan = #10/31/2020 3:12:50 AM#,
      .DatabaseServer = ".\SQLEXPRESS", .Catalog = "NorthWind2020"}
 
    My.Settings.SaveJsonSettings(appSetting)
 
   End If
  End Sub
 End Class
End Namespace

Summary

Alternatives to using My.Setting for persisting and changing values in an application configuration file and using JSON for persisting and changing values in a .JSON file have been presented for fine tuning application settings. Although not complete e.g. there are not wrappers for all types there is enough here to allow a developer to add more wrappers in the ApplicationSettings class and the JSON file classes.

These alternates may not suit everyone but consider moving past conventional windows forms projects where web and Core projects use JSON for configuration settings which gives the reader an opportunity to move forward.

See also

Source code

Clone the following GitHub repository which current contains 59 code sample projects. These 59 projects are all various code samples for future articles. To look at code specific to this article review this project online in the same repository. Also in the same repository check out MyApplicationNamespace project which demostrates other operations regarding settings using a Singleton class.