Browse DevX
Sign up for e-mail newsletters from DevX


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

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




Building the Right Environment to Support AI, Machine Learning and Deep Learning

The Controls.All() Extension Method

The All extension method is a handy mechanism for performing a deep search for all nested controls contained within a Control collection. I found the version shown here on David Findley's blog page, entitled The Über-Find Control (you will see an adaptation of this in the discussion of context menus later in the article):

public static IEnumerable<Control> All(this Control.ControlCollection controls) { foreach (Control control in controls) { foreach (Control grandChild in control.Controls.All()) yield return grandChild; yield return control; } }

This method takes as input a collection of direct child Controls and returns an IEnumerable collection of all descendant controls. The magic in this method stems from the yield statement, also new in C# 3.0. The presence of the yield statement indicates that the method which contains it is an iterator block, which typically means that the method must return some variation of an IEnumerable collection. So this method implicitly creates and returns a collection on the fly. Because LINQ has a great affinity for IEnumerable collections, you can wire this method into a chain of LINQ operators as shown in the goFilterButton_Click method earlier.

The IResourceUser Interface

The Go Filter button code filters the list of controls returned by the All method to just those that implement the IResourceUser interface. The interface is quite simple; it requires implementing only one method, which returns a collection of file names it has processed:

public interface IResourceUser { Collection<string> ResetResources(); }

The ResetAllResources Method

The ResetAllResources method—the method called when a new version is detected—is reasonably straightforward. You pass in one or more forms to process—the main form and any sub-forms. The code uses the same iteration as the Go Filter button in the earlier demo; however rather than displaying only the name and type, because the object is known to implement IResourceUser, the inner loop calls the method required by its contract, ResetResources. The method returns a collection of file names that were purged. It adds that collection to the cumulative names list, and eventually converts that list to a collection and returns it.

public static Collection<string> ResetAllResources(params Form[] forms) { List<string> names = new List<string>(); foreach (Form form in forms) { foreach (Control c in form.Controls.All()) { if (c is IResourceUser) { names.AddRange(((IResourceUser)c).ResetResources()); } } } Console.WriteLine(string.Join(", ", names.ToArray())); return new Collection<string>(names); }

The inner loop takes advantage of the All extension method but does not add any LINQ leverage beyond that. Here's a better rendition that uses LINQ query expression syntax. This query returns just those controls that implement IResourceUser, and then loops through that collection. No conditional code is needed so it is less complex and easier to read.

public static Collection<string> ResetAllResources(params Form[] forms) { List<string> names = new List<string>(); foreach (Form form in forms) { var resourceUsers = from control in form.Controls.All() where control is IResourceUser select control; foreach (var c in resourceUsers) { names.AddRange(((IResourceUser)c).ResetResources()); } } Console.WriteLine(string.Join(", ", names.ToArray())); return new Collection<string>(names); }

In this particular case, you can continue to leverage LINQ but do it more concisely by switching to the lambda syntax. Here you chain the LINQ OfType operator to do exactly what is needed, eliminating both the conditional code and practically the query as well.

public static Collection<string> ResetAllResources(params Form[] forms) { List<string> names = new List<string>(); foreach (Form form in forms) { foreach (var resourceUser in form.Controls.All().OfType<IResourceUser>()) { names.AddRange(resourceUser.ResetResources()); } } Console.WriteLine(string.Join(", ", names.ToArray())); return new Collection<string>(names); }

The next section explains design decisions when implementing the ResetResources method.

The IResourceUser.ResetResources Method

You use the IResourceUser interface when you have embedded file resources that you wish to externalize at first use yet allow for periodic updating. This interface provides a callback to remove the externalized versions so they may be refreshed (typically when a new version is detected).

A typical implementation of the IResourceUser.ResetResources method calls the eponymous static method of the ResourceMgr class. This example erases all XML files tied to this application.

public Collection<string> ResetResources() { return ResourceMgr.ResetResources("*.xml", ref resetResources); } private static bool resetResources;

The Boolean flag lets multiple instances of a class all make the call, even though only the first one will actually reset the resources (because external resources are by definition class resources).

Note that you do not specify a path, just a file name or file name pattern (for multiple files). The application stores externalized files in a subdirectory of the system-defined Application Data directory, where the subdirectory name is the base name of the application. For example, if your application is named MyProgram.exe and the user's is "smith," the folder is …\smith\Application Data\MyProgram.

Author's Note: The initial portion of the path depends upon which version of Windows you are running.

An application with only one form or window is fairly simplistic; it is more realistic to consider those with multiple forms. For such applications you need a different implementation for the ResetResources method. If you refer back to the goFilterButton_Click method of the demo application, you'll notice that there are actually two loops, one that processes controls for the main form, and a second that processes the controls of the sqlEditor control's queryForm. That explicit code references the known subform of one of its controls—but it is impractical for applications to have to know about all of the subforms used by all of its controls. Even if they did initially, library changes could easily change the list of subforms. Instead, the architecture of IResourceUser lets each control be in charge of its own subforms, and to handle these through the ResetResources method. The implementation of this method should include calls to the static ResourceMgr.ResetAllResources method for any of its child forms—the same top-level method called initially. You simply supply a list of arbitrary length enumerating all subforms for the control. This will then search for embedded controls in each. Generically, the code is just:

public Collection<string> ResetResources() { return ResourceMgr.ResetAllResources(subform1, subform2, ...); }

With this in mind, take a look back at Figure 2. Three relevant classes in the CleanCode library implement IResourceUser (see Table 1).

Table 1. Three Implementations of IResourceUser: These three controls each implement IResourceUser, but for different reasons.
Control Reason to Implement IResourceUser Body of ResetResources Method
SqlEditor Contains a subform

return ResourceMgr.ResetAllResources(

ChameleonRichTextBox Uses up to four XML files

return ResourceMgr.ResetResources(
"*"), ref resetResources);

Query Picker Uses one XML file

return ResourceMgr.ResetResources(
ref resetResources);

Each control implements IResourceUser for exactly one of the two possible reasons (subforms or files). If some control happens to qualify on both reasons, then its ResetResources method should include calls to both methods in the ResourceMgr class, combining the collections returned from the two into a single return value.

To summarize, there are two reasons controls need to implement IResourceUser:

  • To manage external resource files used directly by a control.
  • To manage potential external resource files used by any subforms used by a control.
To err on the conservative side, whenever you use a subform you could just assume it has descendants that have resource files to manage. If it does not, there are no harmful consequences; no action will be taken.

Thanks for your registration, follow us on our social networks to keep up-to-date