Managing State and Province Data Across Projects
When you work on multiple web and desktop applications, you’ll often need to display a list of U.S. states, territories, or Canadian provinces. The list can change from one project to the next - sometimes you only want the 50 states, sometimes you include Puerto Rico and the U.S. Virgin Islands, and other times you need all of Canada’s provinces. Adding the same list to each project manually quickly turns into a maintenance nightmare. Every time a new state is added or an abbreviation changes, you have to hunt down each code file, modify it, and recompile the entire solution. The cost in time and the risk of inconsistent data grow quickly.
Hard‑coding the list in code is not only tedious but also fragile. If a project is deployed to several environments, each environment must contain the same hard‑coded list. Any discrepancy can lead to subtle bugs, such as a missing state in a production environment while it appears correctly in development. Moreover, not every developer on a team is comfortable working with databases or XML; a developer who is not familiar with the data source can become a bottleneck. A simple solution that anyone on the team can edit, without needing to rebuild or touch the database, can dramatically reduce friction.
Another common requirement is the ability to switch between full names and abbreviations on the fly. A user interface might show “California (CA)” in a drop‑down list, but the database might store just the abbreviation “CA”. A central place that can map between the two representations keeps the code clean and eliminates the need to hard‑code conversion logic in many spots.
In short, a reusable, editable source for state data that can be shared across projects and that provides quick lookup in both directions is highly desirable. The challenge is to find a format that is both human‑readable and machine‑friendly, that can be edited outside the code base, and that can be accessed quickly at runtime without imposing heavy database dependencies.
XML offers a lightweight solution that satisfies these constraints. By keeping the data in an XML file, you can edit the list in any text editor, share it across projects by copying the file, and load it efficiently into memory when needed. In the next section we’ll walk through designing the XML schema and building a helper module that abstracts all the XML parsing and caching logic.
Building a Simple XML Schema and StateManager
Choosing XML as the storage format lets you keep the state information in a single file that is both machine‑readable and human‑readable. The file can live in the root of a web project or in the bin folder of a WinForms application, making deployment trivial. The XML structure itself is deliberately simple: a root element states encloses multiple state elements, each of which carries a name attribute for the full name and an abbreviation attribute for the short code. Here’s an excerpt of what the file looks like:
Prompt
<?xml version="1.0" encoding="utf-8" standalone="yes" ?></p>
<p><states></p>
<p>
<!-- US States and Territories --></p>
<p>
<state name="Alabama" abbreviation="AL" /></p>
<p>
<state name="Alaska" abbreviation="AK" /></p>
<p>
<state name="Arizona" abbreviation="AZ" /></p>
<p>
<state name="Arkansas" abbreviation="AR" /></p>
<p>
<!-- Canadian Provinces --></p>
<p>
<state name="Alberta" abbreviation="AB" /></p>
<p>
<state name="British Columbia" abbreviation="BC" /></p>
<p></states>
Because the file is tiny, loading it into memory is inexpensive, but repeatedly reading it from disk can still add overhead. To keep the application fast, we load the XML once and cache the resulting objects in the ASP.NET or .NET cache. The cache entry is made dependent on the XML file itself, so if the file changes the cache entry is automatically removed and rebuilt on the next request. This approach keeps the data fresh while avoiding unnecessary I/O.
The code that handles XML parsing lives in a module called StateManager. The module exposes a handful of methods that can be called directly without creating an instance. The core of the module is the getStates function, which returns an array of State objects. The State class itself is a lightweight container holding three properties: Name, Abbreviation, and FullAndAbbrev, which formats the name and abbreviation together. Here’s the class definition:
Prompt
Public Class State</p>
<p>
Private _name As String</p>
<p>
Private _abbreviation As String</p>
<p>
Public Sub New(ByRef nameArg As String, ByRef abbreviationArg As String)</p>
<p>
_name = nameArg</p>
<p>
_abbreviation = abbreviationArg</p>
<p>
End Sub</p>
<p>
Public ReadOnly Property Name() As String</p>
<p>
Get</p>
<p>
Return _name</p>
<p>
End Get</p>
<p>
End Property</p>
<p>
Public ReadOnly Property Abbreviation() As String</p>
<p>
Get</p>
<p>
Return _abbreviation</p>
<p>
End Get</p>
<p>
End Property</p>
<p>
Public ReadOnly Property FullAndAbbrev() As String</p>
<p>
Get</p>
<p>
Return _name & " (" & _abbreviation & ")"</p>
<p>
End Get</p>
<p>
End Property</p>
<p>End Class
With the class in place, the StateManager module takes care of loading the XML, creating State instances, and inserting them into the cache. It also offers convenience methods such as getStateByName and getStateByAbbreviation, which look up a single state by full name or abbreviation. These helpers simplify code in the rest of the application and centralize all data‑access logic in one location. The module also keeps an internal list of errors that can be inspected if something goes wrong during XML parsing or cache population.
Below is the full module code, including cache handling, error tracking, and the lookup helpers. The code is written in VB.NET and is ready to copy into a StateManager.vb file in your project:
Prompt
Imports System, System.Web.Caching, System.Xml, Microsoft.VisualBasic</p>
<p>''' <summary>
''' Provides the functionality related to retrieving the list</p>
<p>''' of states for a system; this is meant for US states,</p>
<p>''' territories, and Canadian provinces. It can also be used</p>
<p>''' for other countries that have states or analogous areas. It</p>
<p>''' uses the States.config file as its data source.</p>
<p>''' </summary>
Public Module StateManager</p>
<p>
' Cache object that will be used to store and retrieve items from</p>
<p>
' the cache and constants used within this object</p>
<p>
Private myCache As Cache = System.Web.HttpRuntime.Cache()</p>
<p>
Private stateKey As String = "StateKey"</p>
<p>
Public applicationConstantsFileName As String = _</p>
<p>
Replace(System.AppDomain.CurrentDomain.BaseDirectory & _</p>
<p>
"States.config", "/", "")</p>
<p>
Private stateArray As State()</p>
<p>
Private errorList As ArrayList</p>
<p>
' Tells you whether or not any errors have occurred w/in the module</p>
<p>
Public ReadOnly Property hasErrors() As Boolean</p>
<p>
Get</p>
<p>
If errorList Is Nothing OrElse errorList.Count = 0 Then</p>
<p>
Return False</p>
<p>
Else</p>
<p>
Return True</p>
<p>
End If</p>
<p>
End Get</p>
<p>
End Property</p>
<p>
' Retrieves an array list of Exception objects</p>
<p>
Public ReadOnly Property getErrors() As ArrayList</p>
<p>
Get</p>
<p>
Return errorList</p>
<p>
End Get</p>
<p>
End Property</p>
<p>
' Private method used to add errors to the errorList</p>
<p>
Private Sub addError(ByRef e As Exception)</p>
<p>
If errorList Is Nothing Then errorList = New ArrayList</p>
<p>
errorList.Add(e)</p>
<p>
End Sub</p>
<p>
''' <summary>
''' Gets all the states</p>
<p>
''' </summary>
''' <returns>An array of State objects</returns>
Public Function getStates() As State()</p>
<p>
If myCache(stateKey) Is Nothing Then</p>
<p>
PopulateCache()</p>
<p>
End If</p>
<p>
Return stateArray</p>
<p>
End Function</p>
<p>
''' <summary>
''' Takes the abbreviation given and returns the full name</p>
<p>
''' </summary>
''' <returns>The full name for the abbreviation in question</returns>
Private Function convertAbbreviationToName(ByRef abbreviation As String) As String</p>
<p>
Dim xmlFile As New XmlDocument()</p>
<p>
Try</p>
<p>
xmlFile.Load(applicationConstantsFileName)</p>
<p>
Dim theNode As XmlNode = _</p>
<p>
xmlFile.SelectSingleNode("descendant::state[@abbreviation='" & abbreviation & "']")</p>
<p>
If Not theNode Is Nothing Then _</p>
<p>
Return theNode.Attributes.GetNamedItem("name").Value</p>
<p>
Catch e As Exception</p>
<p>
addError(e)</p>
<p>
End Try</p>
<p>
Return vbNullString</p>
<p>
End Function</p>
<p>
''' <summary>
''' Gets the state object based on the full name</p>
<p>
''' </summary>
''' <param name="name'>The full name of the state to retrieve</param>
''' <returns>A State object for the name given</returns>
Public Function getStateByName(ByRef name As String) As State</p>
<p>
If myCache(stateKey & name) Is Nothing Then PopulateCache()</p>
<p>
Return myCache(stateKey & name)</p>
<p>
End Function</p>
<p>
''' <summary>
''' Gets the state object based on the abbreviation</p>
<p>
''' </summary>
''' <param name="abbreviation'>The abbreviation of the state to retrieve</param>
''' <returns>A State object for the abbreviation given</returns>
Public Function getStateByAbbreviation(ByRef abbreviation As String) As State</p>
<p>
Dim name As String = convertAbbreviationToName(abbreviation)</p>
<p>
If name
<p>
Return getStateByName(name)</p>
<p>
Else</p>
<p>
Return Nothing</p>
<p>
End If</p>
<p>
End Function</p>
<p>
'''<summary>The manager attempts to load the XML</p>
<p>
''' file and store it in the cache with a dependency on the XML</p>
<p>
''' file itself.' This means that any time the XML file changes, it</p>
<p>
''' is removed from the cache. When the methods that return State</p>
<p>
''' objects are called again, the XML file won't exist in memory</p>
<p>
''' and the PopulateCache will be re-called.</p>
<p>
''' </summary>
Private Sub PopulateCache()</p>
<p>
Dim theState As State</p>
<p>
Dim theNode As XmlNode</p>
<p>
Dim theName, theAbbreviation As String</p>
<p>
Dim i As Integer = 0</p>
<p>
Try</p>
<p>
'Attempt to find the element given the "key" for that tag</p>
<p>
Dim elementList As XmlNodeList = _</p>
<p>
xmlFile.GetElementsByTagName("state")</p>
<p>
If Not elementList Is Nothing Then</p>
<p>
stateArray = Array.CreateInstance(GetType(State), _</p>
<p>
elementList.Count)</p>
<p>
'Loop through each element that has the name we're looking for</p>
<p>
For i=0 To elementList.Count-1</p>
<p>
theNode = elementList(i)</p>
<p>
'Get the name for that tag</p>
<p>
If Not theNode.Attributes.GetNamedItem("name") Is Nothing Then</p>
<p>
theName = theNode.Attributes.GetNamedItem("name").Value</p>
<p>
Else</p>
<p>
theName = vbNullString</p>
<p>
End If</p>
<p>
'Get the abbreviation for that tag</p>
<p>
If Not theNode.Attributes.GetNamedItem("abbreviation") _</p>
<p>
Is Nothing Then</p>
<p>
theAbbreviation = _</p>
<p>
theNode.Attributes.GetNamedItem("abbreviation").Value</p>
<p>
Else</p>
<p>
theAbbreviation = vbNullString</p>
<p>
'Populate that location in the array with the</p>
<p>
' values for the tag</p>
<p>
stateArray(i) = New State(theName, theAbbreviation)</p>
<p>
'Insert the state into cache</p>
<p>
myCache.Insert(stateKey & theName, stateArray(i), _</p>
<p>
New CacheDependency(applicationConstantsFileName))</p>
<p>
Next</p>
<p>
'Insert the state array into cache</p>
<p>
myCache.Insert(stateKey, stateArray, _</p>
<p>
End If</p>
<p>
addError(e)</p>
<p>
End Try</p>
<p>
End Sub</p>
<p>End Module
With the XML schema and the StateManager module in place, the data layer is fully decoupled from the rest of your application. You can now treat state information like any other business object: load it once, cache it, and reference it from anywhere.
Leveraging StateManager in Web and Desktop Apps
Because StateManager is a module, you can call its methods from anywhere in your project without creating an instance. The first step is to place the states.config file in the correct folder: for an ASP.NET web site put it in the root directory; for a WinForms or WPF application place it in the bin folder so that it is copied during the build. Next, add the two code files - State.vb and StateManager.vb - to your project. No additional configuration is required.
To illustrate the power of the module, consider a simple use case: you need the abbreviation for a state that a user selected by full name. The following code demonstrates this scenario in an ASP.NET page code‑behind. It pulls a State object by name and writes the abbreviation to the response:
Prompt
Dim theState As State = StateManager.getStateByName("Virginia")</p>
<p>If Not theState Is Nothing Then</p>
<p>
Response.WriteLine("Abbr: " & theState.Abbreviation)</p>
<p>End If
Often you need a list of all states for a drop‑down selector. The getStates method returns an array of State objects that can be bound directly to a control. In the page’s Page_Load event you bind the list and set the DataTextField and DataValueField properties to expose both the formatted name and the raw full name:
Prompt
Private Sub Page_Load(ByVal sender As System.Object, _</p>
<p>
ByVal e As System.EventArgs) Handles MyBase.Load</p>
<p>
stateList.DataSource = StateManager.getStates()</p>
<p>
stateList.DataTextField = "FullAndAbbrev"</p>
<p>
stateList.DataValueField = "Name"</p>
<p>
stateList.DataBind()</p>
<p>End Sub
Because the module caches the data, this binding is fast even on high‑traffic pages. If the states.config file changes - say a new U.S. territory is added - the cache automatically invalidates and the next request reloads the updated list without requiring a redeploy.
Beyond the drop‑down, the module’s lookup methods make it straightforward to convert between names and abbreviations wherever you need them. For example, when saving a customer record you can store the abbreviation, but when displaying the address you can retrieve the full name from the same module:
Prompt
Dim stateAbbrev As String = GetCustomerStateAbbrev(customerId)</p>
<p>Dim stateObj As State = StateManager.getStateByAbbreviation(stateAbbrev)</p>
<p>Dim displayName As String = stateObj.FullAndAbbrev
In a desktop environment the same pattern applies. If you’re building a form that includes a state selector, you can populate a ComboBox with StateManager.getStates() in the form’s load event. Because the module does not depend on ASP.NET classes, the same code works seamlessly in WinForms, WPF, or even console applications.
Finally, if you ever need to extend the data set - for instance, adding country information or grouping states by region - you can do so directly in the XML file and the module will pick up the changes automatically. The State class can be extended with additional properties, and the module can expose new methods that filter or sort the collection. Since all the data‑access logic is centralized, changes remain local and the rest of your codebase stays untouched.
No comments yet. Be the first to comment!