Share Resources Between Projects

Learn how to share resources among several projects across project boundaries in Visual Studio 2005 and VS 2008. Also, learn about Microsoft's inclusion of IDataErrorInfo in the .NET 3.5 additions to WPF and drill down on how to handle non-standard searches in dictionary classes.

QI put my resources for a given application into a different project because my company wants us to share resources among several projects in our applications. When I do this, I can't see the resources from any other projects. How can I make this work? I'm working in Visual Studio 2005.

AIsolating resources into their own assembly so you can reuse them across projects within and between applications is a logical step, but several details of the implementation make this more difficult than you'd expect. For clarity, this is a question about .NET resources, not Windows Presentation Foundation (WPF) resources. In addition to answering your question, I'll show you improvements in Visual Studio 2008 and give a couple tips that explain how to access C# resource projects from Visual Basic, and vice versa.

When you design resources with the designer in Visual Studio 2005 or Visual Studio 2008, Visual Studio generates a class that exposes your resources as strongly typed properties. This is good. Unfortunately, in Visual Studio 2005, the designer marks these properties internal or Friend, and the only way to change this is to do a global search and replace within those classes, replacing Friend with Public in VB or internal with public in C#. If you have nothing in the project except your resources, you can do the search and replace across the project containing your resources. You must do this every time you make any changes to your resources in the designer. It's ugly, but it works.

Visual Studio 2008 fixes this by providing a combo box at the top of the resource designer that lets you set the scope of resources in the generated file that provides strongly typed access to your resources. If you set this combo box to public, the generated class will set the scope of the properties corresponding to your resources to public, regardless of which version you're targeting. This is a strong reason to move to Visual Studio 2008 as soon as it's practical to do so; using the search and replace in Visual Studio 2005 is a temporary solution.

Unfortunately, you'll discover that namespaces are a bit messy when you're accessing them across assembly boundaries (CSharpResourcesTest and VBResourcesTest are namespaces):

Unfortunately, this is also valid because VB lets you leave off the name of a module:

Dim x4 As String = String1

You can imagine how many current and future collisions you'd have if you had all of your resource values available through direct access syntax.

Fortunately, you can solve this problem and avoid ambiguity collisions between multiple resource projects in both Visual Basic and C# by using namespace aliases. Visual Basic imports also allow you to specify a class in addition to a namespace, and you can alias this namespace for clarity, to remove direct access, and to avoid ambiguities:

Placing resources in their own project is not simple, but it's well worth doing when you have resources shared between projects that should be updated simultaneously.

Q I'm getting some weird results with one of my dictionary classes. After I retrieve a value from a collection, it does not equal the value I selected on. For example, this test code displays "Unexpected result." What's going on?

AThe problem is that the Customer class implements IEquatable<T>. IEquatable<T> is an interface you can implement in any class that redefines the meaning of equality when objects of the class are contained in one of the generic collections, such as List. In your customer class, IEquatable redefined equals:

The original programmer probably used this approach to solve one local problem, but it led to IndexOf and other methods on List that behave differently than you might anticipate throughout the application. These are murky waters because there are several definitions of equality working within .NET and, unless they all align, weird results and bugs happen. In addition to aligning the appropriate methods, you want to achieve consistency in how different business objects treat equality to avoid later confusion.

Default equality behavior is different for reference types and value types. All variables are locations in near memory, also called the stack. A variable pointing to a value type holds the data directly in place on the stack. This works well for small values such as integers and dates. In a reference type the variable location on the stack holds a pointer to a position in far memory, also called the heap. If you set a variable equal to an item of a value type, .NET copies the data into a new location on the stack. If you set a variable equal to an item of a reference type, .NET copies a pointer to the heap location into the new location on the stack and many variables can point to the same instance of a reference type's actual data. This does not occur with value types.

By default, value types are considered equal if all of the values match. This is logical and generally works well. Reference types, however have two possible approaches to equality--either all the values match or the variables are pointing to the same location in memory. To provide the best granularity, by default reference types are considered equal only if the variables point to the same location in far memory; in other words, they are exactly the same object. Occasionally, a type should behave as though it were a value type, even though it's implemented as a reference type. This occurs with strings in the .NET framework, and this justifies .NET allowing overrides on equalities.

The original coder wanted to find an item based on partial value semantics, where the two objects are identical if certain values match. In another scenario, this might arise with primary key comparisons. When you define the equality behavior on the class, it affects every place you use the class, not just the places the original coder had in mind.

Overriding equality is a slippery slope. You must implement equality in a consistent fashion within IComparable, Equals, GetHashCode, and various operators. These operators include equals, not equals, greater than, less than, and so on. All future programmers working with your code also need to understand that you have redefined the meaning of equality. The most disturbing aspect of this is that you'll need to support future additions Microsoft might make. I think it's a bad idea to redefine reference types to use value semantics. I also think that redefining equality for the underlying class is not the easiest way to solve the underlying problem.

If you remove the IEquatable<T> interface the code that inspired the IEquatable<T> interface will probably stop working. Since cust represents a new variable, and not the same pointer value, it's no longer found in the collection. This makes refactoring dangerous, and you'll have to examine all previous usage.

Once you find the code that should search by first and last name, you can replace the original code with one of two approaches. CustomerCollection is a custom class, so you can create a custom method such as "GetByName." If you'd prefer to support direct searches in the list, you can replace the call to IndexOf with a call to the FindIndex method of List. You can also create a method in the Customer class or a helper class that returns a custom delegate:

This works because the anonymous delegate "captures" the value of the local variable custMatch. In Visual Studio 2008, you could use an extension method to retrieve this delegate if you didn't have access to the Customer class, and you could use a lambda expression in either Visual Basic or C#.

You can then use this predicate to find a match based on first and last names:

This accomplishes the original goal, is much clearer with respect to your intent, and avoids changing behavior at other points in the application.

If you elect to redefine equality, do it consistently for all of the equality approaches and across your application. You can use the Is operator in Visual Basic or cast the operands to object before doing the equality comparison to force reference type semantics when you've redefined equality.

Changing the meaning of equality has significant implications to your applications and great capacity to confuse later programmers. Consider it carefully and recognize that there are always other ways to provide value type semantics to accomplish specific tasks.

QThe decorator you described in "WPF Goes to Work" [September 2007] seems like a lot of trouble. Why doesn't Microsoft supply this for us?

A After that article went to press, Microsoft announced that it would include support for IDataErrorInfo in the .NET 3.5 additions to WPF. This is certainly an easier way to accomplish validation if it fits your development model. Your business objects must support IDataErrorInfo, and you must be willing to put the binding rule on every control, assign the binding rule for each control in the window's Load event, or create a decorator that performs the work for you and (possibly) combines authorization.

Isolating your business objects in a middle tier isolates them from changes that result from changes to UI technologies. You rarely want to break this isolation and modify business objects to match UI requirements. However, IDataErrorInfo is a .NET standard for requesting validation information and supporting it provides easier validation in Windows Forms and WPF today. It might also make it easier to perform validation in Silverlight 1.1. If you aren't supporting IDataErrorInfo now, it's worth considering.

If your business objects support IDataErrorInfo, you can supply validation on a per-control basis using two different syntaxes:

The nice thing about validation in WPF is that all the approaches support the same error reporting techniques--specifically, they support the Validation object that appears as a property on many controls, including TextBox. Validation has both a HasError Boolean property and an Errors collection that you can use to style the control in the desired manner:

You'll get some unexpected results if you attempt to use the DataErro­rVali­dationRule in WPF code compiled with .NET 3.0 in Visual Studio 2005. Worse, you won't get an error that will cause the code to fail at compile time; instead, you'll get an error at runtime stating that the XAML cannot be found.

The good news: The new DataErrorValidationRule works if you use the DataErrorValidationRule with Visual Studio 2008 and compile to .NET 3.0 using the multi-targeting feature. This is because Visual Studio uses an updated version of .NET 3.0, whether you're compiling to .NET 3.0 or .NET 3.5. Microsoft calls this a service pack level release, but it's unfortunate Microsoft didn't call this release .NET 3.1 because it introduces important new features to the framework.

You will still need the decorator techniques that I discussed in the "WPF Goes to Work" article for authorization. And, you can modify the decorator approach if your business objects support validation but use a custom mechanism rather than IDataErrorInfo.

Q I want case-insensitive string comparisons in retrieving items from a dictionary. Does this mean I have to loop through the dictionary?

A You can avoid looping through a dictionary of string keys if you define an alternate comparison mechanism. The StringComparer provides several static methods that provide equality and value comparisons. You can pass this to the dictionary constructor:

You can also use the StringComparer.InvariantCultureIgnoreCase or one of the other comparers if that's more appropriate.

Setting the comparer affects the entire key collection. This enables you to achieve a case-insensitive search, as well as disallowing keys that differ only by case. It would be surprising to need keys that differ by case and the ability to use an insensitive search mechanism, but if those were your requirements, you would have to loop through the collection as you suggested.