Practical Techniques: Managing External File Resources
Often an application requires files other than just the executable and associated DLLs, such as XML files, text files, languages files, etc. One approach to file management is to package these files with an installer for your application. A second approach, taken here, is to include these files as resources packaged within your executable or DLL, which are then unpackaged not by an installer but at runtime, by the application itself and only if needed. Often you may allow or even encourage users to edit such externalized files; that is a common reason for making them external. But as an application designer you must decide wisely when—or if—to regenerate these files. You do not, for example, want to recreate them every time the application starts; otherwise your users would lose any changes they might have manually made. A good compromise is to overwrite an existing version of the file only when a new version of an application runs for the first time—and only then if it is not protected with the read-only attribute. This strategy lets each user decide whether to keep user changes from version to version or get a fresh (and possibly updated) version with any new release.
Another advantage is that you need to externalize a file only when it's needed. An application may run many times before that happens.
|Figure 1. Resource Management Algorithm: The basic algorithm for managing externalizable file resources is split into two parts in a common library.|
|Author's Note: The flowchart in Figure 1 illustrates the concept only; it's not an accurate model of the code itself. Your application code invokes ResetResources indirectly, through a higher level ResetAllResources method, which accommodates a more realistic, hierarchical presentation layer.
formalizes this idea into a detailed algorithm. Starting from the application launch, you first check if this is the first time the user has executed this version of the application. If so, your application calls a common library ResetResources
method that determines whether to delete the existing file or leave it alone based on whether a user has explicitly set the read-only attribute to preserve it. After completing this "new version" check, the program continues with other initialization business. At a later point, when you need to use a managed resource file, invoke the InstantiateResource
method to guarantee the file exists. Your execution path arrives at InstantiateResource with your resource in one of these states:
- An existing, write-protected file that the user does not want to update;
- An existing, writable file that still exists because this is not a new version of the application;
- No file if the application was never executed before; or
- No file if this is a new version and the file was not write-protected.
To ensure that the file exists upon return, InstantiateResource
first checks if it already exists. If so, it does nothing further. If the file does not exist, it creates it by externalizing the application resource into a file. In either case, your code may then proceed to use the file with the assurance that it is present.
The algorithm is separated in two parts for several reasons. First, files may be deleted for a variety of reasons unrelated to the running of this application. Good design, therefore, suggests that the application must check for a needed file just before using it or risk a runtime exception if it does not exist. Just as a runtime loads assemblies only when they are about to be used, this technique can generate a file on a just-in-time basis. Because a file may not be needed during any given run of the application, its use may or may not coincide with a new version of the application being run for the first time.
You do not, however, want to generate the file every
time the application runs, because that would potentially overwrite a user's manually entered changes. Furthermore, separating out the pruning step allows it to be encapsulated into a code library with completely generic code. Any and all classes that use file resources can then reuse the library code.
To further explore the workings of this seemingly straightforward algorithm, take a look "under the hood" in the following sections that expound upon its details with actual code examples.
Detecting a New Application Version
A simple technique to detect a new application version is by defining an application setting—call it NewVersion
or something equivalent. Defining this as a Boolean with an initial value of True, you can then include this code in your initialization:
// migrate settings from previous version, if any
// deactive the NewVersion flag for subsequent invocations
Properties.Settings.Default.NewVersion = false;
// Include this here or during your shutdown code to save the above changes
// Reset all file resources associated with this WinForm application
Collection<string> l = ResourceMgr.ResetAllResources(this);
When a new version runs for the first time, it retrieves the NewVersion
setting from the application.exe.config
file, because no user.config
file will exist yet. NewVersion
will therefore be true and the conditional code above will execute. The Upgrade()
call migrates any settings from the previous version—quite useful to ensure you do not lose user customizations. Then the code sets NewVersion
. That value gets stored in the newly created user.config
file with the call to Save
is always considered read-only. Subsequent invocations will still read the application.exe.config
file but will override any settings with values from the user.config
file, so the conditional code will never execute again with this version of the application.
The File Resource Demo Application
The static ResetAllResources
method performs the first part of the algorithm, the part that purges any externalized file resources. Using LINQ queries, the method locates file resources attached to any top level controls or forms plus any attached to their child controls or forms. The File Resource Demo application presented next elucidates the details.
shows a class diagram for a sample WinForm application (MainForm) whose principal control is a SqlEditor
component. This control is itself comprised of two other controls, a ChameleonRichTextBox
and an ExtendedDataGridView
. The SqlEditor control also includes a child form, an instance of an internal StandardQueryPickerForm, which contains a QueryPicker
control. These controls are real, production components that are part of my open-source CleanCode
library. (The hyperlinks on each control take you directly to the API description for each control.) The File Resource Demo application, however, uses stub classes of the same name as each of these controls for illustrative purposes. Figure 3
shows a screenshot of the demo application
. The colored text makes it easy to distinguish the output from each button.
|Figure 2. Class Diagram of the Demo Application: This chart illustrates the relevant associations between hierarchically structured subforms and subcontrols in the File Resource Demo. It also shows the controls that implement the IResourceUser interface, which allows the parent application to find the controls that use managed file resources without having advance knowledge.||
|Figure 3. File Resource Demo Screenshot: This demo application simply shows the color-coded text shown for each button: (1) The Go button reveals the five direct child controls shown on the form. (2) The Go Deep button reveals all nested controls, exposing those inside the nested structure on the left half of the form. (3) The Go Filter button does the same but filters the list to only those that implement the IResourceUser interface as noted in Figure 2.||
Here's a brief description of how each button generates its output, followed by a detailed discussion about how the queries actually work.
The "Go" button simply enumerates all the controls in the application. The Controls
collection of a form includes only its direct children, in this case three buttons used for input, a TextBox used for output, and the SqlEditor control (on the left). This code outputs the text highlighted in green in Figure 3
private void goButton_Click(object sender, EventArgs e)
foreach (Control control in Controls)
The "Go Deep" button uses the All
extension method, which collects not only a form's direct children, but also performs an exhaustive probe for all controls that are descendants of the main form. To accommodate child forms as well as child controls, the code includes a separate loop for the known subform included in the SqlEditor control:
private void goDeepButton_Click(object sender, EventArgs e)
foreach (var control in Controls.All())
foreach (var control in sqlEditor.queryForm.Controls.All())
The "Go Filter" button performs the same operations as Go Deep, but filters the output using a standard LINQ extension method called OfType
to return only those controls that implement IResourceUser:
private void goFilterButton_Click(object sender, EventArgs e)
foreach (var resourceUser in Controls.All().OfType<IResourceUser>())
foreach (var resourceUser in