Login | Register   
RSS Feed
Download our iPhone app
Browse DevX
Sign up for e-mail newsletters from DevX


Using LINQ to Manage File Resources and Context Menus : Page 2

LINQ queries simplify deep introspections of control collections to help manage embedded file resources and context menu initialization.


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.

Figure 1 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:

if (Properties.Settings.Default.NewVersion) { // migrate settings from previous version, if any Properties.Settings.Default.Upgrade(); // 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 Properties.Settings.Default.Save(); // 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 to false. That value gets stored in the newly created user.config file with the call to Save because application.exe.config 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.

Figure 2 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) EmitControlInfo(control); }

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()) EmitControlInfo(control); foreach (var control in sqlEditor.queryForm.Controls.All()) EmitControlInfo(control); }

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>()) EmitControlInfo((Control)resourceUser); foreach (var resourceUser in sqlEditor.queryForm.Controls.All().OfType<IResourceUser>()) EmitControlInfo((Control)resourceUser); }

Comment and Contribute






(Maximum characters: 1200). You have 1200 characters left.